Skip to content

AI scenarios

Attachments

Bit.BlazorUI.AI Scripted transport

Pick, drag, or paste files into the composer. Images preview; everything else gets a card; anything over the limit is turned away before a byte is read.

Live demo

Drag a file onto the composer, paste a screenshot, or use the paperclip, and send: the reply says what arrived and sends it back. Files over 2 MB are turned away, and listed beside the chat.

\n**Three ways in, one limit**: ```razor @implements IAsyncDisposable
``` ```csharp private const long MaxSize = 2 * 1024 * 1024; private readonly List rejections = []; // A transport that reads the files it is sent, and sends them back. private readonly BitAiChatSession session = new(AiDemoContentTransports.ReadsAttachments()); // The composer already tells the user; this is where an application would log or count it. private void OnRejected(BitAiAttachmentRejection rejection) { rejections.Insert(0, rejection); if (rejections.Count > 5) rejections.RemoveAt(rejections.Count - 1); } private static string Describe(BitAiAttachmentRejection rejection) => rejection.Reason switch { BitAiAttachmentRejectionReason.TooLarge => $"{rejection.Size / 1024.0 / 1024.0:0.0} MB is over the limit", BitAiAttachmentRejectionReason.TypeNotAllowed => "that type is not accepted", BitAiAttachmentRejectionReason.TooMany => "more files than can be attached at once", _ => "it could not be read", }; public ValueTask DisposeAsync() => session.DisposeAsync(); ``` `AiDemoContentTransports.cs`: ```csharp // AiScriptedTransport and AiScript are the demo's helpers - see the Basic chat scenario. public static IBitAiTransport ReadsAttachments() => new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder => { // The files are on the user message, as BitAiAttachments. var said = AiScript.LastUserMessage(request); IReadOnlyList files = said is null ? [] : said.Attachments; await yielder(new BitAiMessageStartedEvent()); if (files.Count == 0) { var nothing = $"You said: {said?.GetText()}\n\nAttach a file as well, and I will read it and send it back."; await foreach (var evt in AiScript.Write(nothing, 4, 12, token)) await yielder(evt); await yielder(AiScript.Usage(request, nothing)); await yielder(new BitAiMessageCompletedEvent()); return; } // A small file arrives with its bytes; a larger one is read from the browser when something opens it. var lines = files.Select(f => $"- **{f.Name}**: {f.ContentType ?? "an unknown type"}, " + $"{(f.Size is { } size ? $"{size / 1024.0:0.#} KB" : "size unknown")}, " + (f.HasInlineData ? "sent inline" : "read on demand")); var reply = (files.Count == 1 ? "I received 1 file:" : $"I received {files.Count} files:") + "\n\n" + string.Join("\n", lines) + "\n\n" + "Here " + (files.Count == 1 ? "it is" : "they are") + " back, the way a model returns a file it made:"; await foreach (var evt in AiScript.Write(reply, 4, 12, token)) await yielder(evt); foreach (var file in files) { await AiScript.Pause(250, token); // An image is drawn in the answer - from its data URL, or from the browser's own preview of a file too large to // hold inline; anything else is offered as a file. if (file.Kind is BitAiAttachmentKind.Image && (file.Url ?? file.ThumbnailUrl) is { Length: > 0 } url) { await yielder(new BitAiImageEvent(url) { Alt = file.Name }); } else { await yielder(new BitAiFileEvent(file)); } } await yielder(AiScript.Usage(request, reply)); await yielder(new BitAiMessageCompletedEvent()); })); ```

How it works

  • The paperclip opens the file picker.
  • Dragging files over the composer highlights it, and dropping attaches them.
  • Pasting attaches what the clipboard holds, which is how screenshots actually arrive.

All three share one size limit (MaxAttachmentSize, 2 MB here) and one type filter (the composer's Accept, which a BitAiPromptInputParams cascades into the chat's composer). The size is checked in the browser before a byte is read, and accepted files are streamed rather than sent as one message, so a large paste cannot overrun the Blazor Server message limit. What is turned away is reported through OnAttachmentRejected - the composer already tells the user; that callback is where an app logs or counts it.

Sending puts the files on the user message (BitAiChatSession.SubmitAsync(text, attachments)), so the transport finds them on the request: IBitAiMessage.Attachments, each a BitAiAttachment with its Name, ContentType, Size and Kind. A file within the composer's MaxInlineSize (256 KB) carries its bytes (Data) and a data URL (Url), which suits screenshots; a larger one stays in the browser and is read when something opens it (OpenReadAsync, through its ContentFactory), so nothing large is held in circuit memory. An application that accepts large uploads handles OnSubmit instead, uploads to its own storage, and attaches a URL.

Files go the other way too. A model that generates an image sends it with a BitAiImageEvent (its Url, the Alt text that names it, and a Width and Height that reserve its space), which becomes a BitAiImagePart drawn under the image policy (BitAiUiOptions.Images); any other file arrives as a BitAiFileEvent carrying a BitAiAttachment, which becomes a BitAiFilePart drawn as a chip. This transport lists what it received - inline or read on demand - and sends each file back that way: images as images, the rest as files.

The microphone of AllowDictation appears only where the browser can transcribe, and no audio leaves the device.

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.