Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration! How!?

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.
PropertyDefaultDescription
AiServiceresolved from DIThe IAiConversationService to drive. Leave unset to resolve it from the app’s service provider
BotNameAssistantDisplay name of the AI (also the chat session name)
BotAvatarnullImageSource for the AI
BotBubbleColornullPer-user bubble color for the AI; falls back to OtherBubbleColor
UserNameMeDisplay name of the device user
UserAvatarnullImageSource for the device user
UserBubbleColornullPer-user bubble color for the device user; falls back to MyBubbleColor
LoadHistorytrueBackfill and page history from the message store
GreetingMessagenullAI message shown when there is no history to display
ShowTokenUsagefalseAppends a token usage footer to AI messages when the provider reports usage
ShowMicrophoneActionfalseAdds a push-to-talk action to the input bar; invoking it again cancels the listen
MicrophoneActionText🎤 Voice InputLabel of that action
ShowChoiceButtonstrueRenders a button per AiChoice under AI bubbles that ask a multiple-choice question
ChoiceSendTextSendLabel of the commit button shown for questions that allow more than one choice
SettingsThe 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.