Skip to content

AI scenarios

Citations

Bit.BlazorUI.AI Scripted transport

Numbered markers placed exactly where the claim is made, and a deduplicated source list under the answer.

Live demo

Ask the starter question, then click a citation marker: the source it points at is shown beside the chat.

\n**Anchored citations**: ```razor @implements IAsyncDisposable
@if (selected is { } citation) { @if (citation.Quote is { Length: > 0 } quote) {

“@quote”

} } ``` ```csharp private readonly BitAiChatSession session = new(AiDemoContentTransports.Cited()); private BitAiCitation? selected; private readonly string[] suggestions = ["What happens to state when a circuit drops?"]; private void OnCitationClicked(BitAiCitation citation) => selected = citation; public ValueTask DisposeAsync() => session.DisposeAsync(); ``` `AiDemoContentTransports.cs`: ```csharp // AiScriptedTransport and AiScript are the demo's helpers - see the Basic chat scenario. public static IBitAiTransport Cited() => new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder => { var source = new BitAiCitationSource("s1", "Blazor render modes") { Kind = BitAiSourceKind.Web, Url = "https://learn.microsoft.com/aspnet/core/blazor/components/render-modes", SiteName = "Microsoft Learn", }; await yielder(new BitAiMessageStartedEvent()); await yielder(new BitAiStatusEvent("Searching the documentation")); await AiScript.Pause(700, token); await yielder(new BitAiStatusEvent(null)); const string first = "Interactive Server keeps component state on the server and streams UI diffs over a SignalR circuit."; await foreach (var evt in AiScript.Write(first, 4, 12, token)) await yielder(evt); // Anchored to the sentence it supports, by character offsets into the text. await yielder(new BitAiCitationEvent(new BitAiCitation("c1", source) { StartIndex = 0, EndIndex = first.Length, Quote = "Interactive Server renders the component on the server and sends UI updates over a circuit.", })); // ... the next sentence, cited to the second source the same way. await yielder(new BitAiMessageCompletedEvent()); })); ```

How it works

The transport first shows what it is doing (BitAiStatusEvent("Searching the documentation"), cleared with BitAiStatusEvent(null)), then writes the answer a sentence at a time, sending a BitAiCitationEvent after each sentence it can source. Its BitAiCitation names a BitAiCitationSource - an id, a title, its Kind (web page, document...), its Url, SiteName and Description - and the Quote it relies on. A citation sent without offsets is not anchored anywhere: it only adds its source to the list under the answer.

A BitAiCitation carries character offsets into the text it supports (StartIndex, EndIndex). The renderer inserts a marker at each offset before parsing, so the superscript lands mid-sentence instead of being collected into a footnote nobody connects to anything. The sources the citations name are deduplicated and numbered under the answer, each number matching its markers.

Offsets that reach past the text received so far are held back, so a marker never appears in the wrong sentence while the answer streams. OnCitationClick and OnSourceClick hand the citation or the source to the app, to open it in a panel of its own as this page does.

Components used

Feedback

Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.