# AiChatMessage
**Also known as:** MessageBubble
## Description
BitAiChatMessage is one message of a conversation, laid out by role: a user turn as a bubble at the end of the line (with its attachments, copy and edit), an assistant turn as full-width prose with its parts - text, Markdown, reasoning, tool calls, artifacts, images, generative UI - its error, its sources and its actions, and a system note as a centered pill. It is what BitAiMessageList renders for every message, and what a MessageTemplate wraps to keep all of that.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Animate | `bool` | false | Plays the entrance animation of the message - for one that has just arrived, never for the messages restored from history. It follows the motion tokens, so it is skipped under reduced motion. |
| ChildContent | `RenderFragment?` | null | The extra content rendered at the end of an assistant message, after its parts, its error and its sources. |
| Classes | `BitAiChatMessageClassStyles?` | null | Custom CSS classes for different parts of the chat message. |
| Conversation | `BitAiConversation?` | null | The conversation the message belongs to, which resolves the artifacts its parts refer to. |
| IsLatest | `bool` | true | Whether this is the newest message in the transcript. Regeneration is offered only for the newest reply, because regenerating an older one discards everything after it. It defaults to true, so a reply drawn on its own with a Session offers it; a transcript drawn by hand sets it on the last message only, as BitAiMessageList does. |
| Message | `BitAiMessage` | | The message to render (required). Its Role decides the layout: a user bubble, an assistant reply or a system note. |
| OnCitationClick | `EventCallback<BitAiCitation>` | | The callback for when the user activates a numbered citation in the message. |
| OnFeedback | `EventCallback<BitAiMessageFeedback>` | | The callback for when the user records feedback on this message (or withdraws it), so a host can persist it. The message's Feedback is already updated when it is raised. |
| OnSourceClick | `EventCallback<BitAiCitationSource>` | | The callback for when the user activates a source card in the list of sources under the message. |
| Session | `BitAiChatSession?` | null | The session driving the conversation, which enables editing a user message, regenerating and retrying a reply, and deciding the approval of a tool call or the question of an agent run. |
| Styles | `BitAiChatMessageClassStyles?` | null | Custom CSS styles for different parts of the chat message. |
| AriaLabel | `string?` | null | Gets or sets the accessible label for the component, used by assistive technologies. |
| Class | `string?` | null | Gets or sets the CSS class name(s) to apply to the rendered element. |
| Dir | `BitDir?` | null | Gets or sets the text directionality for the component's content. |
| Disabled | `bool` | false | Gets or sets a value indicating whether the component is disabled and cannot respond to user interaction. |
| ForceAnimation | `bool` | false | Gets or sets a value indicating whether the component's animations play at their full duration even when reduced motion is requested. |
| HtmlAttributes | `Dictionary<string, object>` | new Dictionary<string, object>() | Captures additional HTML attributes to be applied to the rendered element, in addition to the component's parameters. An attribute written on the component wins over an entry of the same name. |
| Id | `string?` | null | Gets or sets the unique identifier for the component's root element. |
| Style | `string?` | null | Gets or sets the CSS style string to apply to the rendered element. |
| TabIndex | `string?` | null | Gets or sets the tab order index for the component when navigating with the keyboard. |
| Visibility | `BitVisibility` | BitVisibility.Visible | Gets or sets the visibility state (visible, hidden, or collapsed) of the component. |
## Public Members
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| UniqueId | `Guid` | Guid.NewGuid() | Gets the readonly unique identifier for the component's root element, assigned when the component instance is constructed. |
| RootElement | `ElementReference` | | Gets the reference to the root HTML element associated with this component. |
## Enums
### BitAiMessageStatus Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Draft | 0 | Composed locally and not yet handed to a transport. |
| Pending | 1 | Submitted; the session is waiting for the first event. A reply with no part yet shows a thinking indicator. |
| Streaming | 2 | Events are arriving and the message is growing. |
| Complete | 3 | The message finished normally. |
| Cancelled | 4 | The message was cancelled by the user or the host; its partial content is kept. |
| Failed | 5 | The message terminated with an error. |
### BitVisibility Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Visible | 0 | The content of the component is visible. |
| Hidden | 1 | The content of the component is hidden, but the space it takes on the page remains (visibility:hidden). |
| Collapsed | 2 | The component is hidden (display:none). |
### BitDir Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Ltr | 0 | Ltr (left to right) is to be used for languages that are written from the left to the right (like English). |
| Rtl | 1 | Rtl (right to left) is to be used for languages that are written from the right to the left (like Arabic). |
| Auto | 2 | Auto lets the user agent decide. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then applies that directionality to the whole element. |
## Sub Classes
### BitAiChatMessageClassStyles Properties
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Root | `string?` | null | Custom CSS classes/styles for the root element of the BitAiChatMessage. |
| System | `string?` | null | Custom CSS classes/styles for the pill a system message is shown in by the BitAiChatMessage. |
| Attachments | `string?` | null | Custom CSS classes/styles for the attachments of a user message of the BitAiChatMessage. |
| Bubble | `string?` | null | Custom CSS classes/styles for the bubble a user message is shown in by the BitAiChatMessage. |
| Actions | `string?` | null | Custom CSS classes/styles for the actions of a user message (copy and edit) of the BitAiChatMessage. |
| EditButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the edit button of a user message of the BitAiChatMessage. |
| Editor | `string?` | null | Custom CSS classes/styles for the editor a user message is edited in by the BitAiChatMessage. |
| EditorActions | `string?` | null | Custom CSS classes/styles for the row of buttons under the editor of the BitAiChatMessage. |
| CancelButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the cancel button of the editor of the BitAiChatMessage. |
| SaveButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the save button of the editor of the BitAiChatMessage. |
| Body | `string?` | null | Custom CSS classes/styles for the body of an assistant message of the BitAiChatMessage. |
| MessageActions | `BitAiMessageActionsClassStyles?` | null | Custom CSS classes/styles for the BitAiMessageActions under an assistant message of the BitAiChatMessage. |
### BitAiMessage Properties
A single message: a role, an ordered list of heterogeneous parts, and the metadata around them. Mutable and observable: streaming mutates it in place and raises Changed for structural changes and PartChanged for content changes inside a part, so a token re-renders one small subtree. Build one with new BitAiMessage(role, messageId?) and AddPart, or let BitAiConversation.AddMessage(role, text) do it; the parts, attachments and citations are copy-on-write snapshots, safe to read while a stream writes.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| MessageId | `string` | | The identifier of the message, given to its constructor or generated. |
| Role | `BitAiRole` | | Who authored it: BitAiRole.User, Assistant, System, Tool or Developer (a string value, so a provider's own role fits too). |
| CreatedAt | `DateTimeOffset` | DateTimeOffset.UtcNow | When the message was created, which a ShowTimestamps option shows under it. |
| Parts | `IReadOnlyList<BitAiMessagePart>` | [] | The parts the message is made of, in order: BitAiTextPart, BitAiMarkdownPart, reasoning, tool calls, artifacts, images, generative UI and the rest. |
| Attachments | `IReadOnlyList<BitAiAttachment>` | [] | The files the message carries, shown as chips above a user bubble. |
| Citations | `IReadOnlyList<BitAiCitation>` | [] | The citations of the message, each numbered by AddCitation (a source cited twice keeps its number). |
| Status | `BitAiMessageStatus` | BitAiMessageStatus.Complete | Where the message is in its lifecycle. A Pending one with no part yet shows a thinking indicator. |
| IsBusy | `bool` | false | True while the message is pending or streaming: its actions are hidden and it is aria-busy. |
| Metadata | `BitAiGenerationMetadata?` | null | What the provider reported about the generation: the model, the token usage, the finish reason, the timings. Merged with MergeMetadata. |
| Error | `BitAiError?` | null | The failure that ended the message, shown with a retry when it is retryable. |
| StatusText | `string?` | null | A transient status line shown while the message is generating ("Searching the documentation"). |
| Feedback / FeedbackComment | `BitAiFeedbackKind / string?` | BitAiFeedbackKind.None / null | The sentiment the user attached to the message, and the free text that came with it. |
| Run | `BitAiAgentRun?` | null | The agent run the message reports, drawn above its parts. |
| Extensions | `Dictionary<string, object?>` | [] | Application-defined values attached to the message. |
| Version | `int` | 0 | A number that goes up on every change of the message, structural or inside a part, for a cheap check of whether anything changed since it was last read. |
| AddPart(part) / InsertPart(index, part) / RemovePart(partId) | `TPart / TPart / bool` | | Adds a part at the end or at a position, or removes one. |
| FindPart(partId) / FindPart<TPart>(partId) / PartsOf<TPart>() / FindToolCall(toolCallId) | `BitAiMessagePart? / TPart? / IEnumerable<TPart> / BitAiToolCallPart?` | | Finds a part by its identifier (of a type, with the generic one), enumerates the parts of a type, or finds a tool call anywhere in the message. |
| GetOrCreateOpenPart<TPart>(kind, factory) / CompleteOpenParts() | `TPart / void` | | What a stream writes through: the last part when it is of that kind and still open, else a new one the factory creates and appends - so text after a tool call starts a new paragraph below it. CompleteOpenParts closes every part still open, for a message that ends abruptly. |
| AddAttachment(attachment) / RemoveAttachment(attachmentId) | `void / bool` | | Adds or removes a file of the message. |
| AddCitation(citation) / GetSources() | `BitAiCitation / IReadOnlyList<BitAiCitationSource>` | | Adds a citation, deduplicated by source and given the next marker number (returned with it); GetSources lists the distinct sources cited, in marker order. |
| SetStatus(status) / SetStatusText(text) / SetError(error) | `void` | | Moves the message to a status, sets its transient status line, or records the failure that ended it. |
| MergeMetadata(metadata) | `void` | | Merges generation metadata, keeping the existing values wherever the incoming record leaves a field null - providers report it piecemeal. |
| SetFeedback(feedback, comment?) | `void` | | Records the user's sentiment about the message, with an optional free-text comment (FeedbackComment). |
| GetText() | `string` | | The text of the message: the text of its parts, the reasoning left out, joined by a blank line - what the copy button copies. |
| Changed / PartChanged | `event Action<BitAiMessage>? / event Action<BitAiMessage, BitAiMessagePart>?` | | Raised after a structural change (a part added or removed, the status moving), and after a part reports a content change. |
### BitAiRole Properties
Who authored a message: a readonly record struct around a string value, compared without regard to case, so a provider's own role converts to one implicitly.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| User / Assistant / System / Tool / Developer | `BitAiRole` | | The well-known roles, "user", "assistant", "system", "tool" and "developer". |
| Value | `string` | | The role as a string, which the message writes into its data-role attribute. |
| IsUser / IsAssistant / IsSystem / IsTool | `bool` | | Which of the well-known roles it is. IsSystem is true for the developer role too: both are instructions to the model rather than turns of the conversation. |
### BitAiMessageFeedback Properties
A feedback decision the user made on a message, raised by the OnFeedback of BitAiChatMessage, BitAiMessageList and BitAiChat so the host can persist it. The BitAiMessageActions under a reply reports only the BitAiFeedbackKind; the message pairs it with itself. A sealed record, compared by value.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Message | `BitAiMessage` | | The message the feedback is about. Its Feedback is already updated. |
| Feedback | `BitAiFeedbackKind` | | The new sentiment, or None when the user withdrew it. |
## CSS Variables
Public CSS custom properties read off the component's root. Set one on `:root` to restyle every instance, on any ancestor to restyle the ones inside it, or on the `Style` of one instance to restyle it and every instance nested inside it, which inherits the value too - a parameter changes one alone, as does the real CSS property on its `Style` / `Styles` when that property does not inherit (`opacity`); an inherited one (`color`, `font-family`) reaches the nested instances that do not set it themselves.
| Name | Default Value | Description |
| :--- | :------------ | :---------- |
| `--bit-AiChatMessage-gap` | spacing(1) | Room between the parts of a message: its attachments, its bubble or body, its actions. |
| `--bit-AiChatMessage-user-background` | var(--bit-clr-bg-sec) | Background of the bubble of a user message. |
| `--bit-AiChatMessage-user-color` | var(--bit-clr-fg-pri) | Text of the bubble of a user message. |
| `--bit-AiChatMessage-user-max-width` | spacing(80), the full width on a phone | Width the bubble of a user message stops growing at. |
| `--bit-AiChatMessage-user-radius` | var(--bit-shp-radius-lg) | Corners of the bubble of a user message, less the one it speaks from, which stays tighter. |
| `--bit-AiChatMessage-assistant-background` | transparent | Background of the body of an assistant message. |
| `--bit-AiChatMessage-assistant-color` | var(--bit-clr-fg-pri) | Text of the body of an assistant message. |
| `--bit-AiChatMessage-body-gap` | spacing(1.5) | Room between the parts of an assistant reply. |
| `--bit-AiChatMessage-system-background` | var(--bit-clr-bg-sec) | Background of the pill of a system message. |
| `--bit-AiChatMessage-system-color` | var(--bit-clr-fg-sec) | Text of the pill of a system message. |
| `--bit-AiChatMessage-system-border-color` | var(--bit-clr-brd-ter) | Border of the pill of a system message. |
## Examples
\n**Basic**:
```razor
```
```csharp
private readonly BitAiMessage userMessage = CreateUserMessage();
private readonly BitAiMessage assistantMessage = CreateReply("Yes. A reply is **full-width prose**, with copy and feedback under it - hover it to see them.");
private readonly BitAiMessage systemMessage = CreateMessage(BitAiRole.System, "The assistant joined the conversation.");
private static BitAiMessage CreateMessage(BitAiRole role, string text)
{
var message = new BitAiMessage(role);
message.AddPart(new BitAiTextPart(text));
return message;
}
// The files a user message carries are shown as chips above its bubble.
private static BitAiMessage CreateUserMessage()
{
var message = CreateMessage(BitAiRole.User, "Can you lay a message out by its role?");
message.AddAttachment(new BitAiAttachment("a1", "layout.pdf") { ContentType = "application/pdf", Size = 182_400 });
return message;
}
// What a provider reports about a reply: the model and the token usage, shown under it.
private static BitAiMessage CreateReply(string text)
{
var message = CreateMessage(BitAiRole.Assistant, text);
message.MergeMetadata(new BitAiGenerationMetadata
{
ModelId = "demo-model-1",
Usage = new BitAiTokenUsage { InputTokens = 42, OutputTokens = 96 },
});
return message;
}
```
\n**Parts**:
```razor
@foreach (var message in partsConversation.Messages.Where(m => m.Role.IsAssistant))
{
}
```
```csharp
// A conversation of varied replies: text, Markdown with code, a tool call with its result, a table with a citation.
private readonly BitAiConversation partsConversation = AiDemoConversations.Long(8);
```
\n**Session**:
```razor
@implements IAsyncDisposable
Ask a question
@foreach (var message in session.Conversation.Messages)
{
}
```
```csharp
// A transport that fails its first answer with a retryable error, and succeeds from then on.
private readonly BitAiChatSession session = new(AiDemoInteractiveTransports.FailsThenRecovers());
protected override void OnInitialized()
{
session.Conversation.MessageAdded += OnSessionMessagesChanged;
session.Conversation.MessageRemoved += OnSessionMessagesChanged;
session.TurnCompleted += OnSessionTurnCompleted;
}
private async Task AskSession()
{
await session.SubmitAsync("What is the weather like in Oslo?");
}
// The messages a turn adds and removes (an edit and a regenerate rewind the thread) are drawn by this loop;
// what changes inside a message is the message's own to render.
private void OnSessionMessagesChanged(BitAiConversation conversation, BitAiMessage message) => _ = InvokeAsync(StateHasChanged);
// And the end of a turn started from inside a message (a retry, an edit, a regenerate) re-enables the Ask button.
private void OnSessionTurnCompleted(BitAiMessage message) => _ = InvokeAsync(StateHasChanged);
public async ValueTask DisposeAsync()
{
session.Conversation.MessageAdded -= OnSessionMessagesChanged;
session.Conversation.MessageRemoved -= OnSessionMessagesChanged;
session.TurnCompleted -= OnSessionTurnCompleted;
await session.DisposeAsync();
}
```
\n**Animate**:
```razor
Add a message
@foreach (var message in animatedMessages)
{
}
```
```csharp
// CreateMessage and CreateReply are the helpers of the Basic example.
private readonly List animatedMessages = [CreateMessage(BitAiRole.User, "Add a few messages below.")];
private void AddAnimatedMessage()
{
animatedMessages.Add(animatedMessages.Count % 2 == 0
? CreateMessage(BitAiRole.User, $"Message {animatedMessages.Count + 1}, from you.")
: CreateReply($"Message {animatedMessages.Count + 1}, from the assistant, sliding in."));
}
```
\n**Child content**:
```razor
Track order #4521
@foreach (var message in disabledSession.Conversation.Messages)
{
}
```
```csharp
// CreateMessage and CreateReply are the helpers of the Basic example.
private bool isDisabled = true;
private readonly BitAiChatSession disabledSession = new(AiDemoTransports.Echo(), CreateDisabledConversation());
private static BitAiConversation CreateDisabledConversation()
{
var conversation = new BitAiConversation();
conversation.AddMessage(CreateMessage(BitAiRole.User, "Hover me: my copy and edit buttons are disabled too."));
conversation.AddMessage(CreateReply("And so are the actions under this reply."));
return conversation;
}
protected override void OnInitialized()
{
disabledSession.Conversation.MessageAdded += OnSessionMessagesChanged;
disabledSession.Conversation.MessageRemoved += OnSessionMessagesChanged;
}
// The messages a regenerate or an edit rewinds and adds (once the toggle is off) are drawn by this loop.
private void OnSessionMessagesChanged(BitAiConversation conversation, BitAiMessage message) => _ = InvokeAsync(StateHasChanged);
public async ValueTask DisposeAsync()
{
disabledSession.Conversation.MessageAdded -= OnSessionMessagesChanged;
disabledSession.Conversation.MessageRemoved -= OnSessionMessagesChanged;
await disabledSession.DisposeAsync();
}
```
\n**Cascading parameters**:
```razor
```
```csharp
// CreateMessage and CreateReply are the helpers of the Basic example.
private readonly BitAiChatMessageParams[] messageParams =
[
new()
{
Styles = new()
{
Bubble = "border:1px solid var(--bit-clr-pri);border-radius:0.5rem",
Body = "padding-inline-start:0.75rem;border-inline-start:3px solid var(--bit-clr-pri)",
},
}
];
private readonly BitAiMessage cascadeUserMessage = CreateMessage(BitAiRole.User, "My bubble takes its style from the BitParams around me.");
private readonly BitAiMessage cascadeAssistantMessage = CreateReply("So does the body of this reply.");
private readonly BitAiMessage cascadeOwnMessage = CreateMessage(BitAiRole.User, "I keep my own Styles, so none of the cascaded ones reach me.");
```
\n**Style & Class**:
```razor
```
```csharp
// CreateMessage and CreateReply are the helpers of the Basic example.
private readonly BitAiMessage styleUserMessage = CreateMessage(BitAiRole.User, "A bubble with a style of its own.");
private readonly BitAiMessage styleAssistantMessage = CreateReply("A reply with a class on its root and its body.");
private readonly BitAiMessage variablesUserMessage = CreateMessage(BitAiRole.User, "A bubble restyled by the variables of its container.");
private readonly BitAiMessage variablesSystemMessage = CreateMessage(BitAiRole.System, "So is this note.");
```
\n**RTL**:
```razor
```
```csharp
// CreateMessage and CreateReply are the helpers of the Basic example.
private readonly BitAiMessage rtlUserMessage = CreateMessage(BitAiRole.User, "میتوانی پیام را بر اساس نقشش بچینی؟");
private readonly BitAiMessage rtlAssistantMessage = CreateReply("بله. پاسخ به صورت **متن تمامعرض** نمایش داده میشود و دکمههای کپی و بازخورد زیر آن هستند.");
```
@(trackingStatus ?? "The order has not been opened yet.")
```
```csharp
// CreateReply is the helper of the Basic example.
private string? trackingStatus;
private readonly BitAiMessage childContentMessage = CreateReply("Your order shipped this morning and should arrive on Thursday.");
private void TrackOrder()
{
trackingStatus = $"Opened the tracking page of order #4521 at {DateTime.Now:t}.";
}
```
\n**Citations & feedback**:
```razor
Last event: @(lastEvent ?? "-")
```
```csharp
// CreateReply is the helper of the Basic example.
private string? lastEvent;
private readonly BitAiMessage citedMessage = CreateCitedMessage();
private static BitAiMessage CreateCitedMessage()
{
const string first = "On the server, for as long as the circuit lives.";
const string second = " When the circuit drops, the state goes with it.";
var reply = CreateReply(first + second);
// Each citation is anchored to the span of the text it supports: its numbered chip goes at the end of it.
reply.AddCitation(new BitAiCitation("c1", new BitAiCitationSource("s1", "Blazor render modes")
{
Kind = BitAiSourceKind.Web,
SiteName = "Microsoft Learn",
Url = "https://learn.microsoft.com/aspnet/core/blazor/components/render-modes",
})
{
StartIndex = 0,
EndIndex = first.Length,
});
reply.AddCitation(new BitAiCitation("c2", new BitAiCitationSource("s2", "SignalR circuit lifetime")
{
Kind = BitAiSourceKind.Document,
SiteName = "Microsoft Learn",
Url = "https://learn.microsoft.com/aspnet/core/blazor/fundamentals/signalr",
})
{
StartIndex = first.Length,
EndIndex = first.Length + second.Length,
});
return reply;
}
private void HandleCitationClick(BitAiCitation citation)
{
lastEvent = $"Citation [{citation.Number}] of \"{citation.Source.Title}\"";
}
private void HandleSourceClick(BitAiCitationSource source)
{
lastEvent = $"Source \"{source.Title}\" ({source.Url})";
}
private void HandleFeedback(BitAiMessageFeedback feedback)
{
lastEvent = $"Feedback {feedback.Feedback}";
}
```
\n**Disabled**:
```razor
@implements IAsyncDisposable