# AiMessagePartView
## Description
BitAiMessagePartView renders one part of a message with the view of its kind: Markdown text, a reasoning panel, a code block, a tool call card, a tool result, an image, a file chip, an artifact card, structured data, a generated component, a source list, the card of a citation or an error. It listens to its part rather than to the message, so a token arriving in the last paragraph of a long answer re-renders that paragraph alone, at most once per render interval. A renderer the app registers for a part kind replaces the built-in view, inside a boundary that contains its failures; a kind no view knows is shown as the text it carries.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Classes | `BitAiMessagePartViewClassStyles?` | null | Custom CSS classes for different parts of the message part view. |
| Conversation | `BitAiConversation?` | null | The conversation, when the part is rendered inside one. An artifact part is resolved against it (the artifact lives on the conversation, the part only points at it), and a registered part renderer is handed it (BitAiMessagePartContext.Conversation). |
| Message | `BitAiMessage` | | The message the part belongs to (required). A text part reads its citations from it, a tool call and structured data hand it on, and a registered part renderer is handed it. |
| OnCitationClick | `EventCallback<BitAiCitation>` | | Raised when the user activates a citation inside this view: a citation chip of a text part, or the card of a citation part. While it is not set, the card of a citation part is a link to its source instead. |
| Part | `BitAiMessagePart` | | The part to render (required), drawn by the view of its kind - or by the renderer registered for its Kind, which is looked up again every time the view's parameters are set. The view subscribes to the part and re-renders whenever it changes, at most once per BitAiUiOptions.RenderInterval; handing it another part moves the subscription to that one. |
| Session | `BitAiChatSession?` | null | The session driving the conversation, when there is one. A tool call hands its approval decisions to it, and a registered part renderer is handed it (BitAiMessagePartContext.Session), so it can act as well as display. |
| Styles | `BitAiMessagePartViewClassStyles?` | null | Custom CSS styles for different parts of the message part view. |
| 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
### 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
### BitAiMessagePartViewClassStyles Properties
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Root | `string?` | null | Custom CSS classes/styles for the root element of the BitAiMessagePartView. |
| Fallback | `string?` | null | Custom CSS classes/styles for the text the BitAiMessagePartView shows for a part of a kind it does not know. |
| Citation | `string?` | null | Custom CSS classes/styles for the container of a citation part of the BitAiMessagePartView. |
| CitationCard | `BitAiSourceCardClassStyles?` | null | Custom CSS classes/styles for the BitAiSourceCard of a citation part of the BitAiMessagePartView. |
| CitationQuote | `string?` | null | Custom CSS classes/styles for the quote of a citation part of the BitAiMessagePartView. |
| CitationLocator | `string?` | null | Custom CSS classes/styles for the locator (a page, a section) of a citation part of the BitAiMessagePartView. |
### BitAiMessagePart Properties
The base class of every part of a message, with an identity and a change notification of its own. Only the part that grew raises Changed while a reply streams, which is what lets one view re-render per token instead of the whole message.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| PartId | `string` | | The identifier of the part, generated unless the transport addresses parts explicitly. A good @key for its view. |
| Kind | `string` | | The discriminator the view is chosen by, and the one a part renderer is registered against. The built-in kinds are the BitAiMessagePartKinds constants; a custom part takes any other. |
| Version | `int` | 0 | Increments on every change, for a cheap ShouldRender of a view of the app's own: compare it with the version seen at the last render. |
| Message | `BitAiMessage?` | null | The message the part belongs to, once it has been added to one. |
| Changed | `event Action<BitAiMessagePart>` | | Raised after the part changes, synchronously on the thread that changed it - a component marshals onto the renderer with InvokeAsync, which the built-in views do for you. |
### BitAiMessagePartKinds Properties
The Kind constants the library ships with, and what this view renders each as. Use them, rather than string literals, when registering a part renderer.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Text | `"text"` | | A BitAiTextPart, the ordinary body of a reply: rendered as streaming Markdown by a BitAiTextPartView, with its citation chips. |
| Markdown | `"markdown"` | | A BitAiMarkdownPart, text explicitly marked as Markdown: rendered like Text. |
| Reasoning | `"reasoning"` | | A BitAiReasoningPart, the model's reasoning: a BitAiReasoningPartView, collapsed unless CollapseReasoning is turned off. |
| Code | `"code"` | | A BitAiCodePart, a standalone code block with a language and an optional file name: a BitAiCodeBlockView. |
| Image | `"image"` | | A BitAiImagePart: a BitAiImagePartView. |
| File | `"file"` | | A BitAiFilePart: a BitAiAttachmentChip of its attachment. |
| Citation | `"citation"` | | A BitAiCitationPart, a single citation placed where it sits between the parts: the BitAiSourceCard of its source, numbered as the message numbers it, with the Quote and the Locator of the citation under it. The card raises OnCitationClick when that is set, and links to the source otherwise. |
| Sources | `"sources"` | | A BitAiSourcesPart, where the source list of the message goes: a BitAiSourceList of the citations it names, or of all the message's citations. |
| ToolCall | `"tool-call"` | | A BitAiToolCallPart: a BitAiToolCallView, or the renderer registered for its tool. |
| ToolResult | `"tool-result"` | | A BitAiToolResultPart, a result carried on its own rather than on its call (the stream processor never adds one; an app does, for a conversation it imports): a BitAiToolResultView. |
| Artifact | `"artifact"` | | A BitAiArtifactPart, pointing at an artifact of the Conversation: a BitAiArtifactPartView. |
| StructuredData | `"structured-data"` | | A BitAiStructuredDataPart, a JSON value that may still be streaming: a BitAiStructuredDataView, or the renderer registered for its schema. |
| GenerativeUi | `"generative-ui"` | | A BitAiGenerativeUiPart, a component the model chose with the properties it supplied: a BitAiGenerativeUiView. |
| Form | `"form"` | | A form the user fills in and sends back. No built-in part carries this kind: a form arrives as structured data (rendered with BitAiForm by a structured renderer), or as a custom part of this kind with a part renderer registered for it. |
| Error | `"error"` | | A BitAiErrorPart, a failure inside an answer that is otherwise worth keeping: a BitAiErrorView. |
### BitAiCustomPart<T> / BitAiCustomJsonPart Properties
The parts for content the library has no concept of. Give one a kind of its own and register a renderer for that kind: it is then rendered, copied and virtualized like a built-in part. A kind with no renderer is shown as its Text.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| BitAiCustomPart<T>(kind, data, partId?) | `constructor` | | A part carrying a typed payload, created by the app. |
| BitAiCustomJsonPart(kind, data, partId?) | `constructor` | | A part carrying a JsonElement payload, for one that arrives as JSON - from a model or a transport. |
| Kind | `string` | | The discriminator a renderer is registered against. Namespace it (acme:invoice) so it never collides with a built-in kind. |
| Data | `T / JsonElement` | | The payload. Setting Data of a BitAiCustomPart<T> notifies its view; after changing the payload in place, call its Refresh(). The data of a BitAiCustomJsonPart is fixed. |
| Text | `string?` | null | The plain-text projection of the part, used when it is copied or exported - and shown by this view when no renderer is registered for the kind. |
### BitAiRendererRegistry Properties
Where an app registers its own renderers, which the components resolve before their built-in views. A singleton: register at startup (AddBitBlazorUIAIServices(ai => ai.ConfigureRenderers(renderers => ...))) and treat the entries as fixed afterwards - every entry is app-wide. A region with renderers of its own cascades a registry of its own (new BitAiRendererRegistry(), in a CascadingValue), which every AI component inside it uses in place of the registered one - in place of it, not on top of it. These are its part members; RegisterTool is on the AiToolCallView page, RegisterArtifact on the AiArtifactView page, RegisterStructured on the AiStructuredDataView page, and RegisterCodeLanguage, MathRenderer and CitationRenderer on the AiMarkdownView page.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| RegisterPart(kind, renderer) | `void` | | Registers a BitAiMessagePartRenderer for a part kind (case does not matter). Every BitAiMessagePartView of a part of that kind renders it in place of its built-in view, inside a BitAiRendererBoundary - a built-in kind included; a later registration replaces an earlier one. |
| RegisterPart<TComponent>(kind) | `void` | | Registers a component as the renderer of a part kind. The component is handed the part as a parameter named Part; declare it with the part type it expects (BitAiCustomJsonPart, BitAiCustomPart<Invoice>). |
| FindPart(kind) | `BitAiMessagePartRenderer?` | | The renderer registered for a part kind, or null when the built-in view draws it. |
### BitAiMessagePartContext Properties
What a custom part renderer is handed (a sealed record). The session is included so a renderer can act as well as display - a custom approval card can call back into the session without the app threading state through cascading values.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Part | `BitAiMessagePart` | | The part to render. |
| Message | `BitAiMessage` | | The message the part belongs to. |
| Conversation | `BitAiConversation?` | null | The conversation, when the part is rendered inside one. |
| Session | `BitAiChatSession?` | null | The session driving the conversation, when there is one. |
### BitAiMessagePartRenderer Properties
The delegate a custom part renderer is: public delegate RenderFragment BitAiMessagePartRenderer(BitAiMessagePartContext context). It is called every time the view's parameters are set, so it builds markup from the context rather than caching it.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| context | `BitAiMessagePartContext` | | The part and its surroundings. |
| (return value) | `RenderFragment` | | The markup drawn in place of the built-in view. For a failure to be contained by the boundary, the code that can throw has to run inside a component the fragment renders, as RegisterPart<TComponent> does. |
## 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-AiMessagePartView-citation-gap` | spacing(0.75) | Room between the card, the quote and the locator of a citation part. |
| `--bit-AiMessagePartView-quote-color` | var(--bit-clr-fg-sec) | Text color of the quote of a citation part. |
| `--bit-AiMessagePartView-quote-border-color` | var(--bit-clr-brd-pri) | Bar down the start edge of the quote of a citation part. |
| `--bit-AiMessagePartView-locator-color` | var(--bit-clr-fg-sec) | Text color of the locator (a page, a section) of a citation part. |
## Examples
\n**Basic**:
```razor
@foreach (var part in basicMessage.Parts)
{
}
```
```csharp
private static JsonElement Json(string json) => JsonDocument.Parse(json).RootElement;
private static BitAiToolCallPart WeatherCall()
{
var call = new BitAiToolCallPart(BitAiId.NewToolCallId(), "get_weather")
{
Definition = new BitAiToolDefinition("get_weather") { Title = "Weather" },
};
call.CompleteArguments(Json("""{"city":"Oslo"}"""));
call.SetStatus(BitAiToolCallStatus.Running);
call.SetResult(BitAiToolOutcome.Success(Json("""{"city":"Oslo","degrees":-3,"condition":"Snow"}""")) with
{
Duration = TimeSpan.FromMilliseconds(700),
});
return call;
}
private readonly BitAiMessage basicMessage = CreateBasicMessage();
private static BitAiMessage CreateBasicMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(new BitAiTextPart("Let me check the weather in Oslo."));
message.AddPart(WeatherCall());
message.AddPart(new BitAiTextPart("It is **-3 °C** and snowing in Oslo - bring a warm coat."));
return message;
}
```
\n**Streaming**:
```razor
Stream an answer
@foreach (var part in streamingMessage.Parts)
{
}
```
```csharp
private const string packingList = """
Pack for **-3 °C** and snow:
- a warm coat and a hat
- gloves and a scarf
- boots with a good grip
The days are short in winter, so plan to be indoors by four.
""";
private BitAiMessage streamingMessage = CreateStreamingMessage();
private bool isStreaming;
// WeatherCall() as in the Basic example.
private static BitAiMessage CreateStreamingMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(WeatherCall());
return message;
}
private async Task StreamAsync()
{
isStreaming = true;
streamingMessage = CreateStreamingMessage();
// An empty text part expects more text, so it streams until Complete is called.
var answer = streamingMessage.AddPart(new BitAiTextPart());
StateHasChanged();
foreach (var chunk in packingList.Chunk(4))
{
await Task.Delay(25);
answer.Append(new string(chunk));
}
answer.Complete();
isStreaming = false;
}
```
\n**Part kinds**:
```razor
@foreach (var part in kindsMessage.Parts)
{
}
```
```csharp
private readonly BitAiMessage kindsMessage = CreateKindsMessage();
private static BitAiMessage CreateKindsMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(new BitAiReasoningPart("The user wants the setup in one line. AddBitBlazorUIAIServices with a transport is all it takes."));
message.AddPart(new BitAiCodePart("builder.Services.AddBitBlazorUIAIServices(ai => ai.UseTransport());", "csharp") { FileName = "Program.cs" });
message.AddPart(new BitAiToolResultPart("call_logs", BitAiToolOutcome.FromText("No errors in the last hour.")) { ToolName = "read_logs" });
message.AddPart(new BitAiErrorPart(new BitAiError("The second search timed out, so this answer is based on the first one only.")
{
Kind = BitAiErrorKind.Timeout,
}));
// No view knows this kind and no renderer is registered for it, so its Text is shown.
message.AddPart(new BitAiCustomPart("demo:poll", ["Tuesday", "Thursday"]) { Text = "Poll: which day suits you, Tuesday or Thursday?" });
return message;
}
```
\n**Citations**:
```razor
@foreach (var part in citedMessage.Parts)
{
}
Delete the archived records
@foreach (var message in planSession.Conversation.Messages)
{
}
```
```csharp
[Inject] private BitAiRendererRegistry Renderers { get; set; } = default!;
protected override void OnInitialized()
{
// An app registers its renderers once, at startup (the Program.cs tab). The registry is shared by the whole app, so
// this page registers its own kind as it loads - the same call, under a kind nothing else uses.
Renderers.RegisterPart("demo:invoice");
}
private static JsonElement Json(string json) => JsonDocument.Parse(json).RootElement;
private const string octoberInvoice = """
{"number":"INV-1042","currency":"EUR","lines":[{"item":"Design review","amount":1200},{"item":"Hosting, 3 months","amount":90}]}
""";
private readonly BitAiMessage invoiceMessage = CreateInvoiceMessage();
private static BitAiMessage CreateInvoiceMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(new BitAiTextPart("Here is the invoice for October:"));
// A part of a kind of its own, carrying the JSON the model produced. Its Text is what a copy of the message holds.
message.AddPart(new BitAiCustomJsonPart("demo:invoice", Json(octoberInvoice)) { Text = "Invoice INV-1042: 1,290.00 EUR" });
return message;
}
```
`Program.cs`:
```csharp
builder.Services.AddBitBlazorUIAIServices(ai =>
{
ai.UseTransport();
// Every part of the kind "demo:invoice", wherever it appears, is drawn by InvoiceCard.
ai.ConfigureRenderers(renderers => renderers.RegisterPart("demo:invoice"));
// Or with a delegate, handed the part, its message, the conversation and the session:
// renderers.RegisterPart("demo:invoice", context => builder =>
// {
// builder.OpenComponent(0);
// builder.AddComponentParameter(1, nameof(InvoiceCard.Part), context.Part);
// builder.CloseComponent();
// });
});
```
`InvoiceCard.razor`:
```razor
@using System.Globalization
@using System.Text.Json
Invoice @number
@code {
// Handed over by the registry, which renders the component with the part as its Part parameter.
[Parameter, EditorRequired] public BitAiCustomJsonPart Part { get; set; } = default!;
private string? number;
private string? currency;
private (string? Item, decimal Amount)[] lines = [];
// Written for the shape the prompt asks the model for. A payload without "lines" throws here - which is what the
// BitAiRendererBoundary the part view renders every registered renderer in contains.
protected override void OnParametersSet()
{
var data = Part.Data;
number = data.GetProperty("number").GetString();
currency = data.GetProperty("currency").GetString();
lines = [.. data.GetProperty("lines").EnumerateArray().Select(line => (line.GetProperty("item").GetString(), line.GetProperty("amount").GetDecimal()))];
}
private string Format(decimal amount) => $"{amount.ToString("N2", CultureInfo.InvariantCulture)} {currency}";
}
```
`InvoiceCard.razor.scss`:
```scss
.invoice-card {
gap: 0.5rem;
display: flex;
max-width: 22rem;
flex-direction: column;
box-sizing: border-box;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
color: var(--bit-clr-fg-pri);
background-color: var(--bit-clr-bg-sec);
border: 1px solid var(--bit-clr-brd-sec);
}
.invoice-card-head {
gap: 0.5rem;
display: flex;
font-weight: 600;
align-items: center;
}
.invoice-card-lines {
margin: 0;
padding: 0;
list-style: none;
}
.invoice-card-lines > li,
.invoice-card-total {
gap: 1rem;
display: flex;
justify-content: space-between;
font-variant-numeric: tabular-nums;
}
.invoice-card-total {
font-weight: 600;
padding-block-start: 0.5rem;
border-block-start: 1px solid var(--bit-clr-brd-sec);
}
```
\n**Renderer failure**:
```razor
@foreach (var part in brokenMessage.Parts)
{
}
```
```csharp
// The model left the lines out of the second invoice, which the card was not written for.
private const string novemberInvoice = """{"number":"INV-1043","currency":"EUR","total":1290}""";
private readonly BitAiMessage brokenMessage = CreateBrokenMessage();
// Json(), octoberInvoice and the registration of InvoiceCard as in the previous example.
private static BitAiMessage CreateBrokenMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(new BitAiTextPart("Here are the invoices for October and November:"));
message.AddPart(new BitAiCustomJsonPart("demo:invoice", Json(octoberInvoice)) { Text = "Invoice INV-1042: 1,290.00 EUR" });
message.AddPart(new BitAiCustomJsonPart("demo:invoice", Json(novemberInvoice)) { Text = "Invoice INV-1043: 1,290.00 EUR" });
message.AddPart(new BitAiTextPart("Both are due at the end of the month."));
return message;
}
```
\n**Renderers of a region**:
```razor
@foreach (var part in pollMessage.Parts)
{
}
@foreach (var part in pollMessage.Parts)
{
}
```
```csharp
private readonly BitAiMessage pollMessage = CreatePollMessage();
private static BitAiMessage CreatePollMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(new BitAiTextPart("Let us find a day for the review."));
message.AddPart(new BitAiCustomPart("demo:poll", ["Tuesday", "Thursday"]) { Text = "Poll: which day suits you, Tuesday or Thursday?" });
return message;
}
// A registry of this region's own: the AI components inside the CascadingValue use it in place of the registered one,
// so it holds every renderer the region needs - here only the poll.
private readonly BitAiRendererRegistry regionRenderers = CreateRegionRenderers();
private static BitAiRendererRegistry CreateRegionRenderers()
{
var registry = new BitAiRendererRegistry();
registry.RegisterPart("demo:poll", RenderPoll);
return registry;
}
// A BitAiMessagePartRenderer: handed the part and its surroundings (context.Message, context.Conversation and
// context.Session as well), it returns the markup drawn in place of the built-in view. The choices are model output,
// so they are only ever written as text.
private static RenderFragment RenderPoll(BitAiMessagePartContext context) => builder =>
{
if (context.Part is not BitAiCustomPart poll) return;
builder.OpenElement(0, "fieldset");
builder.AddAttribute(1, "style", "margin: 0; padding: 0.75rem 1rem; display: flex; gap: 0.5rem; flex-direction: column; border: 1px solid var(--bit-clr-brd-sec); border-radius: 0.5rem;");
builder.OpenElement(2, "legend");
builder.AddAttribute(3, "style", "padding-inline: 0.25rem; font-weight: 600;");
builder.AddContent(4, "Which day suits you?");
builder.CloseElement();
foreach (var choice in poll.Data)
{
builder.OpenElement(5, "label");
builder.AddAttribute(6, "style", "display: flex; gap: 0.5rem; align-items: center;");
builder.OpenElement(7, "input");
builder.AddAttribute(8, "type", "radio");
builder.AddAttribute(9, "name", poll.PartId);
builder.AddAttribute(10, "value", choice);
builder.CloseElement();
builder.AddContent(11, choice);
builder.CloseElement();
}
builder.CloseElement();
};
```
\n**Cascading parameters**:
```razor
```
```csharp
// citedMessage is the one of the Citations example.
private readonly BitAiMessagePartViewParams[] partViewParams =
[
new()
{
Style = "padding-inline-start: 0.75rem; border-inline-start: 3px solid var(--bit-clr-pri);",
Styles = new() { CitationQuote = "color: var(--bit-clr-pri);", CitationLocator = "font-weight: bold;" },
},
];
```
\n**Style & Class**:
```razor
```
```csharp
private readonly BitAiMessage styleMessage = CreateStyleMessage();
private static BitAiMessage CreateStyleMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
var forecast = message.AddCitation(new BitAiCitation("cit_forecast", new BitAiCitationSource("forecast", "Oslo forecast")
{
Url = "https://example.com/weather/oslo",
SiteName = "Weather service",
})
{
Quote = "Snow showers through the afternoon, -3 °C.",
Locator = "Today, 14:00",
});
message.AddPart(new BitAiTextPart("It is **-3 °C** and snowing in Oslo."));
message.AddPart(new BitAiCustomPart("demo:poll", ["Tuesday", "Thursday"]) { Text = "Poll: which day suits you, Tuesday or Thursday?" });
message.AddPart(new BitAiCitationPart(forecast));
return message;
}
```
\n**RTL**:
```razor
@foreach (var part in rtlMessage.Parts)
{
}
```
```csharp
private readonly BitAiMessage rtlMessage = CreateRtlMessage();
private static BitAiMessage CreateRtlMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
message.AddPart(new BitAiTextPart("هوای تهران امروز **۱۸ درجه** و آفتابی است."));
message.AddPart(new BitAiCustomPart("demo:poll", ["سهشنبه", "پنجشنبه"]) { Text = "نظرسنجی: کدام روز برایتان مناسب است، سهشنبه یا پنجشنبه؟" });
return message;
}
```
Activated: @(clickedCitation ?? "none yet")
```
```csharp
private readonly BitAiMessage citedMessage = CreateCitedMessage();
private string? clickedCitation;
private static BitAiMessage CreateCitedMessage()
{
var message = new BitAiMessage(BitAiRole.Assistant);
// Numbered 1 and 2 in the order they are added, which is what [1] and [2] in the text point at.
message.AddCitation(new BitAiCitation("cit_modes", new BitAiCitationSource("render-modes", "Render modes")
{
Url = "https://example.com/docs/render-modes",
SiteName = "Product documentation",
}));
var prerendering = message.AddCitation(new BitAiCitation("cit_prerender", new BitAiCitationSource("prerendering", "Prerendering")
{
Url = "https://example.com/docs/prerendering",
SiteName = "Product documentation",
})
{
Quote = "Prerendering renders the page on the server first, so it is painted before its components become interactive.",
Locator = "Section 3: Prerendering",
});
message.AddPart(new BitAiTextPart("Interactive Server keeps the components on the server [1], and prerendering paints them once before they become interactive [2]."));
// The second citation placed where it sits, between the parts: the card of its source, numbered 2 as the message
// numbers it, with the quote and the locator under it.
message.AddPart(new BitAiCitationPart(prerendering));
return message;
}
```
\n**Conversation & Session**:
```razor
@implements IAsyncDisposable
@(message.Role.IsUser ? "You" : "Assistant")
@foreach (var part in message.Parts)
{
}
}
```
```csharp
[Inject] private IBitAiToolRegistry ToolRegistry { get; set; } = default!;
private BitAiRenderScheduler sessionRenders = default!;
private BitAiChatSession planSession = default!;
protected override void OnInitialized()
{
// The session raises StateChanged on every event it applies; the scheduler turns those into at most one render
// of the page per frame interval.
sessionRenders = new BitAiRenderScheduler(() => InvokeAsync(StateHasChanged));
// A scripted model that asks for delete_records, then answers once the tool has returned. The tool itself is the
// one registered with AddBitBlazorUIAIServices, declared Destructive, so the default approval policy asks before it
// runs. The session continues a conversation of its own.
planSession = new BitAiChatSession(
AiDemoToolTransports.CallsTool(
"delete_records",
"""{"filter":"status = 'archived'"}""",
"Deleted 128 archived records."),
conversation: CreatePlanConversation(),
tools: ToolRegistry,
approvalPolicy: new BitAiDefaultToolApprovalPolicy());
planSession.StateChanged += sessionRenders.Request;
}
// A conversation whose first answer wrote an artifact, the way a session leaves it: the artifact belongs to the
// conversation, and the part of the answer only names it.
private static BitAiConversation CreatePlanConversation()
{
var conversation = new BitAiConversation();
var plan = conversation.GetOrAddArtifact("art-cleanup", BitAiArtifactKind.Document, "Clean-up plan");
plan.SetContent("""
## Clean-up plan
1. Archive the records nobody opened this year.
2. Delete the archived records.
""");
plan.Complete("First draft");
conversation.AddMessage(BitAiRole.User, "Plan the clean-up of the old records.");
var answer = conversation.AddMessage(new BitAiMessage(BitAiRole.Assistant));
answer.AddPart(new BitAiTextPart("Here is the plan:"));
answer.AddPart(new BitAiArtifactPart("art-cleanup"));
return conversation;
}
private Task AskToDeleteAsync() => planSession.SubmitAsync("Delete the archived records");
public async ValueTask DisposeAsync()
{
await planSession.DisposeAsync();
await sessionRenders.DisposeAsync();
}
```
\n**Custom part renderer**:
```razor
@foreach (var part in invoiceMessage.Parts)
{
-
@foreach (var (item, amount) in lines)
{
- @item @Format(amount) }
Total
@Format(lines.Sum(line => line.Amount))