# AiStreamingMarkdown
## Description
BitAiStreamingMarkdown renders Markdown that is still being written, as a BitAiMarkdownView that follows the text while it grows. It holds one incremental parser for its lifetime, so the text already seen is parsed once however many deltas arrive - a long streamed answer costs linear work rather than quadratic. While the text streams, a caret trails it, its last block stays open, a code block is highlighted only once its fence closes and a dangling emphasis or code delimiter is hidden until its partner arrives.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Citations | `IReadOnlyList<BitAiCitation>?` | null | The citations that the inline citation markers of the text resolve against: a bracketed number such as [1] is drawn as a BitAiCitationMarker when a citation of that Number is here (and BitAiUiOptions.InlineCitations is on); one that matches none stays the literal text. |
| Classes | `BitAiStreamingMarkdownClassStyles?` | null | Custom CSS classes for different parts of the streaming Markdown. |
| IsStreaming | `bool` | false | Whether more text is expected. While it is true the streaming caret trails the text (unless BitAiUiOptions.ShowStreamingCaret is off), the last block is left open - a code block is not highlighted, a dangling emphasis or code delimiter is hidden - and turning it false settles the document. |
| OnCitationClick | `EventCallback<BitAiCitation>` | | The callback for when the user activates an inline citation marker, with the citation it stands for. |
| Styles | `BitAiStreamingMarkdownClassStyles?` | null | Custom CSS styles for different parts of the streaming Markdown. |
| Text | `string?` | null | The Markdown received so far. Only the text after the last block boundary already seen is parsed again when it grows; a text that is not a continuation of the previous one (an edit, a new answer) is parsed afresh. |
| 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
### BitAiStreamingMarkdownClassStyles Properties
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Root | `string?` | null | Custom CSS classes/styles for the root element of the BitAiStreamingMarkdown. |
| Markdown | `BitAiMarkdownViewClassStyles?` | null | Custom CSS classes/styles for the parts of the BitAiMarkdownView the BitAiStreamingMarkdown renders its text with. |
### BitAiIncrementalMarkdownParser Properties
The parser a BitAiStreamingMarkdown holds for its lifetime, for an app that renders streamed Markdown with a BitAiMarkdownView of its own (ShowCaret while it streams). It keeps the blocks before the last point no later text can change - the start of a line after a blank line that begins a new top-level block - and parses only the tail again on every call, so a long answer costs linear work rather than quadratic. A text with a link reference or footnote definition, which makes any text depend on any other, is parsed whole on every call. An instance belongs to one streaming text and is not thread-safe.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| BitAiIncrementalMarkdownParser(BitAiMarkdownOptions? options = null) | `constructor` | | Creates a parser for the given extensions (BitAiMarkdownOptions.Default when null). Make a new one when the options change. |
| Parse(string? source, bool isComplete = false) | `BitAiMarkdownDocument` | | Parses the whole text received so far - not just the new part - and returns a new document whose blocks may be shared with the previous one. isComplete false leaves the last block open; a source that does not continue the previous one is parsed afresh. |
| Reset() | `void` | | Discards all cached state, so the next parse starts from scratch. |
| StableLength | `int` | 0 | How many characters of the source have been promoted to the stable prefix that is never parsed again - for diagnostics and tests: the measure of how much work is being reused. |
| StableBlockCount | `int` | 0 | How many blocks are in the stable prefix. |
### BitAiRenderScheduler Properties
Coalesces the change notifications of a stream - hundreds of deltas a second - into a bounded number of renders, the way the AI components do with BitAiUiOptions.RenderInterval. Rendering every delta is wasted work in WebAssembly and a flood of diffs down the circuit of Blazor Server; the scheduler makes the frame rate matter rather than the throughput. While a render is in flight, further requests only mark it dirty and one more render follows, so a slow renderer falls behind in render count, never in memory. Dispose it with the page (IAsyncDisposable).
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| BitAiRenderScheduler(Func<Task> flush, TimeSpan? interval = null) | `constructor` | | Creates a scheduler that calls flush when a render is due - in a component, () => InvokeAsync(StateHasChanged) - at most once per interval (DefaultInterval when null). |
| DefaultInterval | `TimeSpan` | 50 ms | Static: one render every 50 milliseconds, twenty a second - continuous to the eye, and comfortable over a Blazor Server circuit on a poor connection. |
| Request() | `void` | | Asks for a render. Cheap and safe to call from any thread, on every token. |
| FlushAsync() | `Task` | | Renders at once, bypassing the interval: call it when the stream ends, so the final state is never left waiting behind a timer. |
| RenderCount | `int` | 0 | How many renders the scheduler has performed, for tests and diagnostics. |
| CoalescedCount | `int` | 0 | How many requests have been coalesced away. |
| DisposeAsync() | `ValueTask` | | Stops the scheduler: a pending render is dropped, and later requests are ignored. |
## Examples
\n**Basic**:
```razor
```
```csharp
private const string basicMarkdown = """
### Two render modes
Blazor renders on the **server**, over a circuit, or in the **browser**, on WebAssembly. Pick one per page with
`@rendermode`, or let `InteractiveAuto` start on the server and move to the browser once it has downloaded.
""";
```
\n**Streaming**:
```razor
@implements IAsyncDisposable
Stream the answer
Stop
```
```csharp
private const string streamingAnswer = """
## Comparing render modes
The **short answer** is that it depends on *where* your users are.
| Mode | Runs on | First paint |
| --- | --- | :---: |
| Server | The server, over a circuit | Fast |
| WebAssembly | The browser | Slower |
```csharp
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
```
- [x] Measure the first paint
- [ ] Measure the cost of every update
> Rendering more often than the eye can follow is not smoother. It is just louder.
""";
private string streamedText = string.Empty;
private bool isStreaming;
private bool slowMotion;
private CancellationTokenSource? streamCts;
private BitAiRenderScheduler? scheduler;
private async Task StreamAsync()
{
streamCts = new CancellationTokenSource();
scheduler ??= new BitAiRenderScheduler(() => InvokeAsync(StateHasChanged));
streamedText = string.Empty;
isStreaming = true;
try
{
// A few characters at a time, the way a model streams its tokens.
for (var i = 0; i < streamingAnswer.Length; i += 4)
{
await Task.Delay(slowMotion ? 150 : 20, streamCts.Token);
streamedText += streamingAnswer.Substring(i, Math.Min(4, streamingAnswer.Length - i));
scheduler.Request();
}
}
catch (OperationCanceledException) { }
finally
{
isStreaming = false;
await scheduler.FlushAsync();
}
}
private void StopStreaming() => streamCts?.Cancel();
public async ValueTask DisposeAsync()
{
streamCts?.Cancel();
if (scheduler is not null) await scheduler.DisposeAsync();
}
```
`BitAiStreamingMarkdownDemo.razor.scss`:
```scss
.stream-grid {
gap: 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}
.stream-grid > div {
min-width: 0;
}
.stream-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.stream-raw {
margin: 0;
padding: 0.75rem;
overflow: auto;
min-height: 6rem;
max-height: 28rem;
font-size: 0.8125rem;
white-space: pre-wrap;
border-radius: 0.5rem;
background-color: var(--bit-clr-bg-sec);
}
```
\n**Citations**:
```razor
```
```csharp
private const string lineBreakMarkdown = """
Server rendering is fast to first paint.
WebAssembly works offline.
Auto starts on one and moves to the other.
""";
private const string danglingMarkdown = "The **short answer** is that it depends on **where";
private bool keepLineBreaks;
private bool hideDanglingDelimiters = true;
private BitAiUiOptions markdownOptions = BitAiUiOptions.Default;
// A new options instance only when a choice changes: one made on every render would rebuild the parser every time.
private void UpdateMarkdownOptions() => markdownOptions = BitAiUiOptions.Default with
{
Markdown = BitAiMarkdownOptions.Default with
{
SoftBreaksAsHardBreaks = keepLineBreaks,
HideDanglingDelimiters = hideDanglingDelimiters,
},
};
```
\n**Cascading parameters**:
```razor
```
```csharp
private const string styleMarkdown = """
#### A styled answer
- a first point
- a second point
""";
private readonly BitAiStreamingMarkdownParams[] streamingParams =
[
new() { Styles = new() { Markdown = new() { Heading = "color: var(--bit-clr-pri);" } } }
];
```
\n**Style & Class**:
```razor
```
```csharp
private const string styleMarkdown = """
#### A styled answer
- a first point
- a second point
""";
```
\n**RTL**:
```razor
```
```csharp
private const string rtlMarkdown = """
#### دو حالت رندر
بلیزر روی **سرور** یا در **مرورگر** رندر میکند و پاسخ هنوز در حال نوشته شدن است
""";
```
Received so far
@streamedText
Rendered
Activated: @(clickedSource ?? "none yet")
```
```csharp
private const string citedMarkdown = """
Interactive Server keeps component state on the server and streams UI diffs over a SignalR circuit [1].
If that circuit drops, the state goes with it unless you have persisted it yourself [2]. A number no
citation has, such as [3], stays the text it is.
""";
private readonly BitAiCitation[] citations =
[
new("c1", new BitAiCitationSource("s1", "Blazor render modes")
{
Url = "https://learn.microsoft.com/aspnet/core/blazor/components/render-modes",
SiteName = "Microsoft Learn",
}) { Number = 1 },
new("c2", new BitAiCitationSource("s2", "SignalR circuit lifetime")
{
Url = "https://learn.microsoft.com/aspnet/core/blazor/fundamentals/signalr",
SiteName = "Microsoft Learn",
}) { Number = 2 },
];
private string? clickedSource;
```
\n**Markdown options**:
```razor