Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

MAUI Chat UI

Shiny.AiConversation.Maui ships AiChatView — a complete chat screen for your AI conversation. It derives from the Shiny.Maui.Controls ChatView, so every style, template and behavior property of the base control still applies, but the provider, session, history paging and live AI events are already wired up.

There is no view model plumbing: the control resolves IAiConversationService from the app’s service provider.

Terminal window
dotnet add package Shiny.AiConversation.Maui

The package brings Shiny.Maui.Controls with it. Register the conversation service as usual in MauiProgram.cs (see Getting Started) and make sure .UseShinyControls() is called on the MauiAppBuilder.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ai="http://shiny.net/maui/aiconversation"
x:Class="MyApp.ChatPage"
Title="Chat">
<ai:AiChatView BotName="Aura"
BotAvatar="bot.png"
GreetingMessage="Hi! What can I help you with?"
ShowMicrophoneAction="True"
ShowTokenUsage="True"
MyBubbleColor="{StaticResource Primary}"
MyTextColor="White"
OtherBubbleColor="#F0EEFF"
OtherTextColor="#1A1A2E"
BubbleCornerRadius="16"
PlaceholderText="Ask me something..."
SendButtonText="Send" />
</ContentPage>
  • Sending — typed messages are forwarded to IAiConversationService.TalkTo; replies arrive on AiResponded and render as AI bubbles.
  • Voice — utterances heard by speech-to-text render as user bubbles via SpeechOccurred. That covers wake-word conversations happening elsewhere in the app, so the transcript stays complete even when the user never types. ShowMicrophoneAction adds a push-to-talk action to the input bar that calls ListenAndTalk.
  • History — the chat is backfilled from the registered IMessageStore and pages further back when the user scrolls to the top. With no message store registered, the chat simply starts empty and stays live-only — nothing throws.
  • Typing indicator — driven by AiState (Thinking / Responding), with a heartbeat so a long turn keeps the bubble alive.
  • Errors — failures from TalkTo and anything raised on ErrorOccurred render as an AI bubble carrying Identifier = "error", so a MessageTemplateSelector can style them.
  • Multiple choice — a turn that carries choices renders tappable buttons under the bubble; the tapped label is sent as the user’s answer.
Property Default Description
AiService resolved from DI The IAiConversationService to drive. Leave unset to resolve it from the app’s service provider
BotName Assistant Display name of the AI (also the chat session name)
BotAvatar null ImageSource for the AI
BotBubbleColor null Per-user bubble color for the AI; falls back to OtherBubbleColor
UserName Me Display name of the device user
UserAvatar null ImageSource for the device user
UserBubbleColor null Per-user bubble color for the device user; falls back to MyBubbleColor
LoadHistory true Backfill and page history from the message store
GreetingMessage null AI message shown when there is no history to display
ShowTokenUsage false Appends a token usage footer to AI messages when the provider reports usage
ShowMicrophoneAction false Adds a push-to-talk action to the input bar; invoking it again cancels the listen
MicrophoneActionText 🎤 Voice Input Label of that action
ShowChoiceButtons true Renders a button per AiChoice under AI bubbles that ask a multiple-choice question
ChoiceSendText Send Label of the commit button shown for questions that allow more than one choice
Settings The live AiChatSettings behind the properties above
Refresh() Method — tears down and reloads the conversation (use after ClearChatHistory)

Styling is entirely inherited from ChatViewMyBubbleColor, MyTextColor, OtherBubbleColor, OtherTextColor, ChatBackgroundColor, BubbleFontSize, BubbleFontFamily, BubbleCornerRadius, TimestampFontSize, PlaceholderText, SendButtonText, SendButtonBackgroundColor, SendButtonTextColor, InputBarBackgroundColor, InputBarBorderColor, IsInputBarVisible, ShowTypingIndicator, MessageTemplate, MessageTemplateSelector, InputActions, CustomBubbleActions, PageSize, UseFeedback and AdjustForKeyboard all behave exactly as documented for ChatView. Leave the colors unset to follow the active Shiny theme.

If you would rather lay out the chat yourself, register the bridge and bind Provider + SessionId on a regular ChatView:

builder.Services.AddShinyAiConversation(opts =>
{
opts.AddGithubCopilotChatClient();
opts.AddChatSessionProvider(cfg =>
{
cfg.BotName = "Aura";
cfg.ShowTokenUsage = true;
});
});

That registers an AiChatSessionProvider as IChatSessionProvider, configured by the same AiChatSettings object the control uses.

When the AI asks a question with a fixed set of answers, AiChatView renders each AiChoice as a chip under the bubble. Tapping one sends its Label back as the user’s answer — the model resolves it in context, so nothing is matched locally.

<ai:AiChatView ShowChoiceButtons="True" ChoiceSendText="Confirm" />

Questions with AllowMultiple toggle their chips on and off and commit with a send chip, so the user picks several before anything is sent. Once an answer goes out, that bubble’s chips are disabled.

A custom MessageTemplate or MessageTemplateSelector replaces the whole bubble body, so the built-in choice selector is only installed when you have set neither. To render choices in your own template, read them off the message metadata:

public class MyTemplateSelector : DataTemplateSelector
{
protected override DataTemplate? OnSelectTemplate(object item, BindableObject container)
{
if (item is ChatMessage message && AiChoiceTemplateSelector.ReadQuestions(message) is { Length: > 0 } questions)
{
// questions[i].Choices[j].Label - render however you like
return this.MyChoiceTemplate;
}
return null; // null falls back to the control's default bubble
}
}

The metadata key is AiChoiceTemplateSelector.QuestionsMetadataKey and the payload round-trips through AiTurnSerializer.SerializeQuestions / DeserializeQuestions.

Set ShowChoiceButtons="False" to turn the feature off entirely.