@page "/" @attribute [StreamRendering(true)] @inject AgentDiscoveryClient AgentClient @inject IJSRuntime JSRuntime @inject ILogger Logger @inject IActorClient ActorClient @rendermode InteractiveServer @using System.Text @using System.Text.Json @using Microsoft.Extensions.AI @using Microsoft.Extensions.AI.Agents @using Microsoft.Extensions.AI.Agents.Hosting @using Microsoft.Extensions.AI.Agents.Runtime Agent Web Chat

Agent Web Chat

The best hypertext-based chat on the Web!

@if (!string.IsNullOrEmpty(selectedAgentName) && currentConversation == null) { }
@if (conversations.Any()) {
@foreach (var conv in conversations) {
@GetAgentIcon(conv.AgentName) @GetAgentDisplayName(conv.AgentName)
}
} @if (currentConversation != null) {
@foreach (var message in currentConversation.Messages) {
@if (message.Role != ChatRole.User) {
@GetAgentIcon(currentConversation.AgentName)
}
@message.Text
@(message.Role == ChatRole.User ? "You" : GetAgentDisplayName(currentConversation.AgentName))
} @if (isStreaming && currentStreamedMessage.Length > 0) {
@GetAgentIcon(currentConversation.AgentName)
@currentStreamedMessage
}
}
@code { private string currentMessage = ""; private bool isStreaming = false; private bool isLoadingAgents = true; private string currentStreamedMessage = ""; private string selectedAgentName = ""; private List availableAgents = new(); private List conversations = new(); private Conversation? currentConversation; private class Conversation { public string SessionId { get; set; } = Guid.NewGuid().ToString(); public string AgentName { get; set; } = ""; public List Messages { get; set; } = new(); } protected override async Task OnInitializedAsync() { Logger.LogDebug("Initializing Agent Chat component"); // Load agents try { availableAgents = await AgentClient.GetAgentsAsync(); Logger.LogInformation("Loaded {AgentCount} agents", availableAgents.Count); // Default to first agent and start a conversation if (availableAgents.Any()) { selectedAgentName = availableAgents.First().Name!; StartNewConversation(); } } catch (Exception ex) { Logger.LogError(ex, "Failed to load agents"); } finally { isLoadingAgents = false; } // Conversations start fresh on page load } private string GetAgentIcon(string agentName) { return agentName?.ToLower() switch { "pirate" => "🏴‍☠️", "knights-and-knaves" => "⚔️", _ => "🤖" }; } private string GetAgentDisplayName(string agentName) { return agentName?.ToLower() switch { "pirate" => "Pirate", "knights-and-knaves" => "Knights & Knaves", _ => agentName ?? "Agent" }; } private void StartNewConversation() { if (string.IsNullOrEmpty(selectedAgentName)) return; var newConversation = new Conversation { AgentName = selectedAgentName }; conversations.Add(newConversation); currentConversation = newConversation; Logger.LogInformation("Started new conversation with agent: {AgentName}, session: {SessionId}", newConversation.AgentName, newConversation.SessionId); StateHasChanged(); } private void SelectConversation(string sessionId) { currentConversation = conversations.FirstOrDefault(c => c.SessionId == sessionId); if (currentConversation != null) { selectedAgentName = currentConversation.AgentName; Logger.LogDebug("Selected conversation with session: {SessionId}", sessionId); } StateHasChanged(); } private void CloseConversation(string sessionId) { var conversationToRemove = conversations.FirstOrDefault(c => c.SessionId == sessionId); if (conversationToRemove != null) { conversations.Remove(conversationToRemove); if (currentConversation?.SessionId == sessionId) { currentConversation = conversations.FirstOrDefault(); if (currentConversation != null) { selectedAgentName = currentConversation.AgentName; } } Logger.LogInformation("Closed conversation with session: {SessionId}", sessionId); } StateHasChanged(); } private async Task SendMessage() { if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming || currentConversation == null) return; var userMessage = currentMessage.Trim(); currentMessage = ""; Logger.LogInformation("User sending message: '{UserMessage}' to agent {AgentName} in session {SessionId}", userMessage, currentConversation.AgentName, currentConversation.SessionId); // Add user message to chat currentConversation.Messages.Add(new ChatMessage(ChatRole.User, userMessage)); StateHasChanged(); await ScrollToBottom(); // Start streaming response isStreaming = true; currentStreamedMessage = ""; StateHasChanged(); try { var responseContent = new StringBuilder(); var agent = new AgentProxy(currentConversation.AgentName, ActorClient); var thread = agent.GetNewThread(); thread.ConversationId = currentConversation.SessionId; await foreach (var update in agent.RunStreamingAsync( [new ChatMessage(ChatRole.User, userMessage)], thread, cancellationToken: CancellationToken.None)) { var content = update.Text ?? ""; if (!string.IsNullOrEmpty(content)) { responseContent.Append(content); currentStreamedMessage = responseContent.ToString(); StateHasChanged(); await ScrollToBottom(); } } // Add the complete agent response to chat messages if (responseContent.Length > 0) { currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, responseContent.ToString())); } else { currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, "Sorry, I couldn't generate a response.")); } } catch (Exception ex) { Logger.LogError(ex, "Error occurred while processing message in session {SessionId}: {ErrorMessage}", currentConversation.SessionId, ex.Message); currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, $"Error: {ex.Message}")); } finally { isStreaming = false; currentStreamedMessage = ""; StateHasChanged(); await ScrollToBottom(); } } private bool ShouldPreventDefault = false; private async Task HandleKeyPress(KeyboardEventArgs e) { if (e.Key == "Enter" && !e.ShiftKey) { ShouldPreventDefault = true; await SendMessage(); ShouldPreventDefault = false; } else if (e.Key == "Escape") { currentMessage = ""; // Clear input on Escape ShouldPreventDefault = true; StateHasChanged(); ShouldPreventDefault = false; // Reset after clearing } else { ShouldPreventDefault = false; } } private async Task ScrollToBottom() { try { await JSRuntime.InvokeVoidAsync("scrollToBottom", "chat-messages"); } catch (Exception ex) { Logger.LogWarning(ex, "Failed to scroll to bottom"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JSRuntime.InvokeVoidAsync("eval", @" window.scrollToBottom = function(elementId) { const element = document.getElementById(elementId); if (element) { requestAnimationFrame(() => { element.scrollTop = element.scrollHeight; }); } }; "); } } }