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

Agent Web Chat

The best hypertext-based chat on the Web!

@if (!string.IsNullOrEmpty(selectedAgentName) && currentConversation is null) { }
@switch (selectedProtocol) { case Protocol.OpenAIResponses: ֎ OpenAI Responses break; case Protocol.OpenAIChatCompletions: ֎ OpenAI ChatCompletions break; case Protocol.A2A: default: 🔗 A2A protocol supports long-running agentic processes break; }
@if (selectedProtocol == Protocol.A2A) {

A2A Configuration

Discover and configure agent cards
@if (isA2AExpanded) {
@if (!string.IsNullOrEmpty(selectedAgentName)) { for agent: @GetAgentDisplayName(selectedAgentName) } else { Please select an agent first }
@if (discoveredAgentCardJson is not null) {

🔗 Discovered Agent Card

Agent Card JSON:
@discoveredAgentCardJson
} @if (!string.IsNullOrEmpty(discoveryError)) {
@discoveryError
}
}
} @if (conversations.Any()) {
@foreach (var conv in conversations) {
@GetAgentIcon(conv.AgentName) @GetAgentDisplayName(conv.AgentName)
}
} @if (currentConversation is not 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; // protocol private Protocol selectedProtocol; // a2a agent card private bool isA2AExpanded = false; private bool isDiscoveringCard = false; private string? discoveredAgentCardJson = null; private string? discoveryError = null; private enum Protocol { A2A, // Agent-to-Agent protocol OpenAIResponses, OpenAIChatCompletions } private sealed class Conversation { public string SessionId { get; set; } = Guid.NewGuid().ToString("N"); 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); Logger.LogInformation("Loaded Agents info: {AgentData}", JsonSerializer.Serialize(availableAgents, new JsonSerializerOptions() { WriteIndented = true })); // 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) => agentName?.ToLower() switch { "pirate" => "🏴‍☠️", "knights-and-knaves" => "⚔️", _ => "🤖" }; private string GetAgentDisplayName(string agentName) => agentName?.ToLower() switch { "pirate" => "Pirate", "knights-and-knaves" => "Knights & Knaves", _ => agentName ?? "Agent" }; private void ToggleA2AExpanded() => isA2AExpanded = !isA2AExpanded; private async Task DiscoverAgentCard() { if (string.IsNullOrEmpty(selectedAgentName) || isDiscoveringCard) return; isDiscoveringCard = true; discoveryError = null; discoveredAgentCardJson = null; StateHasChanged(); try { Logger.LogInformation("Discovering agent card for agent: {AgentName}", selectedAgentName); var agentCard = await A2AActorClient.GetAgentCardAsync(selectedAgentName); if (agentCard is not null) { discoveredAgentCardJson = JsonSerializer.Serialize(agentCard, new JsonSerializerOptions() { WriteIndented = true }); Logger.LogInformation("Successfully discovered agent card for {AgentName}: {CardData}", selectedAgentName, discoveredAgentCardJson); } else { discoveryError = "No agent card found for this agent."; } } catch (Exception ex) { Logger.LogError(ex, "Failed to discover agent card for {AgentName}", selectedAgentName); discoveryError = $"Failed to discover agent card: {ex.Message}"; } finally { isDiscoveringCard = false; StateHasChanged(); } } 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 is not 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 is not null) { conversations.Remove(conversationToRemove); if (currentConversation?.SessionId == sessionId) { currentConversation = conversations.FirstOrDefault(); if (currentConversation is not null) { selectedAgentName = currentConversation.AgentName; } } Logger.LogInformation("Closed conversation with session: {SessionId}", sessionId); } StateHasChanged(); } private async Task SendMessage() { if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming || currentConversation is 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(); StringBuilder responseContent = new(); var hasReceivedContent = false; using var timeoutCts = new CancellationTokenSource( #if DEBUG TimeSpan.FromSeconds(120) #else TimeSpan.FromSeconds(20) #endif ); try { // Select the appropriate client based on protocol AgentClientBase agentClient = selectedProtocol switch { Protocol.OpenAIResponses => OpenAIResponsesAgentClient, Protocol.OpenAIChatCompletions => OpenAIChatCompletionsAgentClient, Protocol.A2A or _ => A2AActorClient }; var messages = new List { new(ChatRole.User, userMessage) }; await foreach (var update in agentClient.RunStreamingAsync( currentConversation.AgentName, messages, currentConversation.SessionId, cancellationToken: timeoutCts.Token)) { var content = update.Text ?? ""; if (!string.IsNullOrEmpty(content)) { hasReceivedContent = true; responseContent.Append(content); currentStreamedMessage = responseContent.ToString(); StateHasChanged(); await ScrollToBottom(); Logger.LogDebug("Received streaming content: {ContentLength} characters", content.Length); } } Logger.LogInformation("Streaming completed for session {SessionId}, total content length: {ContentLength}", currentConversation.SessionId, responseContent.Length); // Add the complete agent response to chat messages if (responseContent.Length > 0) { currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, responseContent.ToString())); } else if (!hasReceivedContent) { Logger.LogWarning("No content received during streaming for session {SessionId}", currentConversation.SessionId); currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, "No response received from the agent.")); } else { currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, "Sorry, I couldn't generate a response.")); } } catch (OperationCanceledException) when (isStreaming) { Logger.LogWarning("Streaming operation timed out for session {SessionId}", currentConversation.SessionId); currentConversation.Messages.Add(new ChatMessage(ChatRole.Assistant, "Request timed out. Please try again.")); } 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; }); } }; "); } } }