More UI cleanups

This commit is contained in:
Javier Calvarro Nelson
2025-12-11 16:10:59 +01:00
Unverified
parent c0ce673354
commit fcb6ec9fd3
15 changed files with 145 additions and 34 deletions
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Default template for rendering ErrorContent. Renders nothing by default
/// to prevent the error from crashing the UI. More specific templates can
/// override this to display error messages.
/// </summary>
public class ErrorTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
this.ChildContent = this.RenderError;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Determines if this template should handle the given content.
/// Matches ErrorContent.
/// </summary>
public override bool When(ContentContext context)
{
return context.Content is ErrorContent;
}
private RenderFragment RenderError(ContentContext content) => builder =>
{
// By default, render nothing.
// Specific templates can override to display error messages.
};
}
@@ -35,7 +35,7 @@ public class FunctionCallTemplate : ContentTemplateBase
/// </summary>
/// <param name="context">The content context.</param>
/// <returns>True if this template should render the content.</returns>
public new bool When(ContentContext context)
public override bool When(ContentContext context)
{
if (context.Content is not FunctionCallContent call)
{
@@ -53,32 +53,8 @@ public class FunctionCallTemplate : ContentTemplateBase
private RenderFragment RenderFunctionCall(ContentContext content) => builder =>
{
if (content.Content is FunctionCallContent call)
{
// Get or create the invocation context which tracks call and result
var invocation = this.Context.GetOrCreateInvocation(call);
builder.OpenComponent<CascadingValue<InvocationContext>>(0);
builder.AddComponentParameter(1, "Value", invocation);
builder.AddComponentParameter(2, "IsFixed", true);
builder.AddComponentParameter(3, "ChildContent", (RenderFragment)(innerBuilder =>
{
// Default fallback rendering - shows function name and loading/result status
innerBuilder.OpenElement(0, "div");
innerBuilder.AddAttribute(1, "class", "function-call");
innerBuilder.AddContent(2, $"Function: {call.Name}");
if (invocation.HasResult)
{
innerBuilder.AddContent(3, " [Result available]");
}
else
{
innerBuilder.AddContent(3, " [Loading...]");
}
innerBuilder.CloseElement();
}));
builder.CloseComponent();
}
// By default, function calls are not rendered visually.
// Custom templates (like WeatherCallTemplate) can override this
// behavior for specific functions by registering before this template.
};
}
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.AspNetCore.Components.AI;
/// <summary>
/// Default template for FunctionResultContent that renders nothing.
/// Function results are internal tool responses and typically don't need
/// visual representation in the chat UI.
/// </summary>
public class FunctionResultTemplate : ContentTemplateBase
{
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
public override void Attach(RenderHandle renderHandle)
{
// This component never renders anything by itself.
this.ChildContent = this.RenderFunctionResult;
}
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.RegisterContentTemplate(this);
return Task.CompletedTask;
}
/// <summary>
/// Only match FunctionResultContent.
/// </summary>
public override bool When(ContentContext context) => context.Content is FunctionResultContent;
private RenderFragment RenderFunctionResult(ContentContext content) => builder =>
{
// By default, function results are not rendered visually.
// The result data is typically processed by the agent to generate text responses.
};
}
@@ -21,6 +21,12 @@ public class TextTemplate : ContentTemplateBase
return Task.CompletedTask;
}
/// <summary>
/// Only match TextContent, allowing other content templates to handle
/// FunctionCallContent, FunctionResultContent, etc.
/// </summary>
public override bool When(ContentContext context) => context.Content is TextContent;
private RenderFragment RenderText(ContentContext content) => builder =>
{
if (content.Content is TextContent textContent)
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
@@ -52,8 +52,10 @@ internal sealed class DefaultMessageTemplate : MessageTemplateBase
/// <summary>
/// Checks if a message has any visible content that should be displayed.
/// Messages that only contain FunctionCallContent or FunctionResultContent
/// are internal tool messages and should not be rendered as chat bubbles.
/// Messages that only contain FunctionResultContent are internal tool messages
/// and should not be rendered as chat bubbles.
/// FunctionCallContent is considered visible because content templates can render them
/// (e.g., WeatherCallTemplate renders weather tool calls as cards).
/// </summary>
private static bool HasVisibleContent(ChatMessage message)
{
@@ -70,8 +72,15 @@ internal sealed class DefaultMessageTemplate : MessageTemplateBase
return true;
}
// Other content types that are not function calls/results are visible
if (content is not FunctionCallContent && content is not FunctionResultContent)
// FunctionCallContent is visible - content templates can render them
// (e.g., WeatherCallTemplate renders weather tool calls as cards)
if (content is FunctionCallContent)
{
return true;
}
// Other content types that are not function results are visible
if (content is not FunctionResultContent)
{
return true;
}
@@ -27,6 +27,14 @@ public partial class Messages : IComponent
{
builder.OpenComponent<TextTemplate>(0);
builder.CloseComponent();
builder.OpenComponent<FunctionCallTemplate>(1);
builder.CloseComponent();
builder.OpenComponent<FunctionResultTemplate>(2);
builder.CloseComponent();
builder.OpenComponent<ErrorTemplate>(3);
builder.CloseComponent();
builder.OpenComponent<DataContentTemplate>(4);
builder.CloseComponent();
};
public void Attach(RenderHandle renderHandle)
@@ -28,6 +28,10 @@
<ContentTemplates>
<TaskProgressTemplate />
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
@@ -20,7 +20,12 @@
<div class="chat-content">
<Messages>
<ContentTemplates>
<TextTemplate />
<WeatherCallTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
@@ -29,7 +29,7 @@ public class WeatherCallTemplate : ContentTemplateBase
/// Determines if this template should handle the given content.
/// Matches FunctionCallContent for the get_weather function.
/// </summary>
public new bool When(ContentContext context)
public override bool When(ContentContext context)
{
// Only match FunctionCallContent for the get_weather function
return context.Content is FunctionCallContent call &&
@@ -24,6 +24,10 @@
<ContentTemplates>
<CreatePlanCallTemplate />
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
@@ -52,6 +52,10 @@
<Messages>
<ContentTemplates>
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
@@ -114,6 +114,10 @@
<Messages>
<ContentTemplates>
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
@@ -28,6 +28,10 @@
<ContentTemplates>
<HaikuCallTemplate />
<TextTemplate />
<FunctionCallTemplate />
<FunctionResultTemplate />
<ErrorTemplate />
<DataContentTemplate />
</ContentTemplates>
</Messages>
<AgentLoadingIndicator />
@@ -283,6 +283,11 @@ h1:focus {
line-height: 1.5;
}
/* Hide empty message bubbles (e.g., function calls with no visual template) */
.chat-message:empty {
display: none;
}
/* User messages - right aligned with blue background */
.chat-message.user-message {
align-self: flex-end;