mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Add AG-UI Blazor sample
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.ToolBasedGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a haiku with Japanese and English translations, along with display properties.
|
||||
/// </summary>
|
||||
public class Haiku
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the three lines of the haiku in Japanese.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Japanese { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the three lines of the haiku translated to English.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> English { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the image associated with the haiku.
|
||||
/// </summary>
|
||||
public string? ImageName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CSS gradient for the haiku card background.
|
||||
/// </summary>
|
||||
public string Gradient { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// List of valid image names that can be used with haikus.
|
||||
/// </summary>
|
||||
public static readonly string[] ValidImageNames =
|
||||
[
|
||||
"Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg",
|
||||
"Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg",
|
||||
"Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg",
|
||||
"Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg",
|
||||
"Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg",
|
||||
"Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg",
|
||||
"Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg",
|
||||
"Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg",
|
||||
"Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg",
|
||||
"Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default placeholder haiku.
|
||||
/// </summary>
|
||||
public static Haiku CreatePlaceholder() => new()
|
||||
{
|
||||
Japanese = ["仮の句よ", "まっさらながら", "花を呼ぶ"],
|
||||
English = ["A placeholder verse—", "even in a blank canvas,", "it beckons flowers."],
|
||||
ImageName = null,
|
||||
Gradient = string.Empty
|
||||
};
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.AI;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.ToolBasedGenerativeUI;
|
||||
|
||||
/// <summary>
|
||||
/// Template for rendering generate_haiku function call content.
|
||||
/// Renders a HaikuCard component inline in the chat messages.
|
||||
/// </summary>
|
||||
public class HaikuCallTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
// This component never renders anything by itself.
|
||||
this.ChildContent = this.RenderHaikuCall;
|
||||
}
|
||||
|
||||
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 FunctionCallContent for the generate_haiku function.
|
||||
/// </summary>
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
// Only match FunctionCallContent for the generate_haiku function
|
||||
return context.Content is FunctionCallContent call &&
|
||||
string.Equals(call.Name, "generate_haiku", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private RenderFragment RenderHaikuCall(ContentContext content) => builder =>
|
||||
{
|
||||
if (content.Content is FunctionCallContent call)
|
||||
{
|
||||
// Get the invocation context which tracks both call and result
|
||||
var invocation = this.Context.GetOrCreateInvocation(call);
|
||||
|
||||
// Provide the invocation context to child components
|
||||
builder.OpenComponent<CascadingValue<InvocationContext>>(0);
|
||||
builder.AddComponentParameter(1, "Value", invocation);
|
||||
builder.AddComponentParameter(2, "IsFixed", true);
|
||||
builder.AddComponentParameter(3, "ChildContent", (RenderFragment)(innerBuilder =>
|
||||
{
|
||||
// Render the HaikuCard component which uses InvocationContext
|
||||
innerBuilder.OpenComponent<HaikuCard>(0);
|
||||
innerBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
|
||||
<div class="haiku-card @(IsLoading ? "haiku-loading" : "")" style="@GetBackgroundStyle()">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="haiku-content">
|
||||
<div class="haiku-lines">
|
||||
<div class="haiku-line skeleton-line"></div>
|
||||
<div class="haiku-line skeleton-line"></div>
|
||||
<div class="haiku-line skeleton-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (CurrentHaiku is not null)
|
||||
{
|
||||
<div class="haiku-content">
|
||||
<div class="haiku-lines">
|
||||
@for (int i = 0; i < CurrentHaiku.Japanese.Count && i < 3; i++)
|
||||
{
|
||||
var index = i;
|
||||
<div class="haiku-line" style="animation-delay: @(index * 100)ms">
|
||||
<p class="japanese-text" data-testid="haiku-japanese-line">@CurrentHaiku.Japanese[index]</p>
|
||||
@if (index < CurrentHaiku.English.Count)
|
||||
{
|
||||
<p class="english-text" data-testid="haiku-english-line">@CurrentHaiku.English[index]</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(CurrentHaiku.ImageName))
|
||||
{
|
||||
<div class="haiku-image-container">
|
||||
<img src="images/@CurrentHaiku.ImageName"
|
||||
alt="@CurrentHaiku.ImageName"
|
||||
class="haiku-image"
|
||||
data-testid="haiku-image" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets the haiku to display from the cascaded InvocationContext.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
public InvocationContext? Invocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the haiku to display directly (used when not rendering from a tool call).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Haiku? Haiku { get; set; }
|
||||
|
||||
private Haiku? CurrentHaiku => Haiku ?? GetHaikuFromInvocation();
|
||||
private bool IsLoading => CurrentHaiku is null;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Invocation is not null && !Invocation.HasResult)
|
||||
{
|
||||
Invocation.ResultArrived += OnResultArrived;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResultArrived()
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Haiku? GetHaikuFromInvocation()
|
||||
{
|
||||
if (Invocation?.HasResult == true)
|
||||
{
|
||||
return Invocation.GetResult<Haiku>();
|
||||
}
|
||||
|
||||
// Try to get partial data from arguments while streaming
|
||||
if (Invocation is not null)
|
||||
{
|
||||
var japanese = Invocation.GetArgument<string[]>("japanese");
|
||||
var english = Invocation.GetArgument<string[]>("english");
|
||||
var imageName = Invocation.GetArgument<string>("image_name");
|
||||
var gradient = Invocation.GetArgument<string>("gradient");
|
||||
|
||||
if (japanese is not null && japanese.Length > 0)
|
||||
{
|
||||
return new Haiku
|
||||
{
|
||||
Japanese = japanese,
|
||||
English = english ?? [],
|
||||
ImageName = imageName,
|
||||
Gradient = gradient ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetBackgroundStyle()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CurrentHaiku?.Gradient))
|
||||
{
|
||||
return $"background: {CurrentHaiku.Gradient};";
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/* Haiku Card Styles */
|
||||
.haiku-card {
|
||||
background: linear-gradient(120deg, #ffffff 0%, #fdfdfd 50%, #ffffff 100%);
|
||||
background-size: 200% 200%;
|
||||
animation: animated-gradient 10s ease infinite;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: 10px solid #ff6f61;
|
||||
padding: 2rem 2.5rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.01),
|
||||
0 0 15px rgba(255, 111, 97, 0.25);
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
margin: 1.5rem auto;
|
||||
transition: transform 0.35s ease, box-shadow 0.35s ease, border-top-width 0.35s ease, border-top-color 0.35s ease;
|
||||
}
|
||||
|
||||
.haiku-card:hover {
|
||||
transform: translateY(-8px) scale(1.03);
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.01),
|
||||
0 0 25px rgba(255, 91, 74, 0.5);
|
||||
border-top-width: 14px;
|
||||
border-top-color: #ff5b4a;
|
||||
}
|
||||
|
||||
.haiku-card.haiku-loading {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
@keyframes animated-gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes fade-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Haiku Content */
|
||||
.haiku-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.haiku-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.haiku-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
animation: fade-slide-in 0.5s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.japanese-text {
|
||||
font-family: serif;
|
||||
font-weight: bold;
|
||||
font-size: 2.5rem;
|
||||
background: linear-gradient(to right, #1e293b, #475569);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.english-text {
|
||||
font-weight: 300;
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
max-width: 400px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Skeleton Loading */
|
||||
.skeleton-line {
|
||||
width: 80%;
|
||||
height: 3rem;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Image Container */
|
||||
.haiku-image-container {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.haiku-image {
|
||||
width: 100%;
|
||||
max-height: 320px;
|
||||
object-fit: cover;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.haiku-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 768px) {
|
||||
.haiku-card {
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.japanese-text {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.english-text {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.haiku-image {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
|
||||
<div class="haiku-carousel-container" data-testid="haiku-carousel">
|
||||
@if (Haikus.Count > 1)
|
||||
{
|
||||
<button class="carousel-button carousel-prev" @onclick="PreviousHaiku" disabled="@(currentIndex == 0)">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
|
||||
<div class="carousel-content">
|
||||
@if (Haikus.Count > 0 && currentIndex < Haikus.Count)
|
||||
{
|
||||
<div class="carousel-item" data-testid="carousel-item-@currentIndex">
|
||||
<HaikuCard Haiku="@Haikus[currentIndex]" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Haikus.Count > 1)
|
||||
{
|
||||
<button class="carousel-button carousel-next" @onclick="NextHaiku" disabled="@(currentIndex >= Haikus.Count - 1)">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Haikus.Count > 1)
|
||||
{
|
||||
<div class="carousel-indicators">
|
||||
@for (int i = 0; i < Haikus.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<button class="indicator @(index == currentIndex ? "active" : "")"
|
||||
@onclick="() => GoToHaiku(index)">
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private int currentIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of haikus to display in the carousel.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IReadOnlyList<Haiku> Haikus { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Event callback when the current haiku index changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<int> OnIndexChanged { get; set; }
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// Reset to first haiku when new haiku is added at the beginning
|
||||
if (Haikus.Count > 0 && currentIndex >= Haikus.Count)
|
||||
{
|
||||
currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NextHaiku()
|
||||
{
|
||||
if (currentIndex < Haikus.Count - 1)
|
||||
{
|
||||
currentIndex++;
|
||||
await OnIndexChanged.InvokeAsync(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PreviousHaiku()
|
||||
{
|
||||
if (currentIndex > 0)
|
||||
{
|
||||
currentIndex--;
|
||||
await OnIndexChanged.InvokeAsync(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GoToHaiku(int index)
|
||||
{
|
||||
if (index >= 0 && index < Haikus.Count)
|
||||
{
|
||||
currentIndex = index;
|
||||
await OnIndexChanged.InvokeAsync(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the carousel to show the first (newest) haiku.
|
||||
/// </summary>
|
||||
public void ResetToFirst()
|
||||
{
|
||||
currentIndex = 0;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/* Haiku Carousel Styles */
|
||||
.haiku-carousel-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.carousel-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
width: 100%;
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation Buttons */
|
||||
.carousel-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.carousel-button:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
border-color: #ff6f61;
|
||||
color: #ff6f61;
|
||||
box-shadow: 0 4px 12px rgba(255, 111, 97, 0.2);
|
||||
}
|
||||
|
||||
.carousel-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.carousel-button svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Carousel Indicators */
|
||||
.carousel-indicators {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #e2e8f0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.indicator:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
.indicator.active {
|
||||
background: #ff6f61;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 768px) {
|
||||
.haiku-carousel-container {
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.carousel-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.carousel-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
@* Copyright (c) Microsoft. All rights reserved. *@
|
||||
@using Microsoft.AspNetCore.Components.AI
|
||||
@using Microsoft.Agents.AI
|
||||
@using Microsoft.Extensions.AI
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using AGUIDojoClient.Components.Shared
|
||||
@using System.ComponentModel
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<PageTitle>Tool Based Generative UI</PageTitle>
|
||||
|
||||
<div class="tool-generative-ui-layout">
|
||||
<div class="main-display">
|
||||
<HaikuCarousel @ref="carouselRef" Haikus="@haikus" />
|
||||
</div>
|
||||
|
||||
<div class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">Haiku Generator</div>
|
||||
<button class="new-chat-button" @onclick="ResetConversation">
|
||||
<span class="button-icon">+</span> New chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AgentBoundary Agent="@agent" OnContextCreated="OnContextCreated">
|
||||
<div class="chat-content">
|
||||
<Messages>
|
||||
<ContentTemplates>
|
||||
<HaikuCallTemplate />
|
||||
<TextTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Ask for a haiku..." />
|
||||
</div>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private HaikuCarousel? carouselRef;
|
||||
private List<Haiku> haikus = [Haiku.CreatePlaceholder()];
|
||||
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Nature Haiku", new ChatMessage(ChatRole.User, "Write me a haiku about nature.")),
|
||||
new Suggestion("Ocean Haiku", new ChatMessage(ChatRole.User, "Create a haiku about the ocean.")),
|
||||
new Suggestion("Spring Haiku", new ChatMessage(ChatRole.User, "Generate a haiku about spring."))
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "tool_based_generative_ui";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("tool-based-generative-ui");
|
||||
}
|
||||
|
||||
private void OnContextCreated(IAgentBoundaryContext context)
|
||||
{
|
||||
// Register the generate_haiku tool as a frontend tool
|
||||
var generateHaikuTool = AIFunctionFactory.Create(
|
||||
(string[] japanese, string[] english, string? image_name, string? gradient) =>
|
||||
GenerateHaiku(japanese, english, image_name, gradient),
|
||||
"generate_haiku",
|
||||
$"Generate a haiku with Japanese text, English translation, and an optional image. Valid image names: {string.Join(", ", Haiku.ValidImageNames)}");
|
||||
|
||||
context.RegisterTool(generateHaikuTool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frontend tool handler that creates a new haiku and adds it to the carousel.
|
||||
/// </summary>
|
||||
[Description("Generate a haiku with Japanese and English text, plus an optional image.")]
|
||||
private Haiku GenerateHaiku(
|
||||
[Description("3 lines of haiku in Japanese")] string[] japanese,
|
||||
[Description("3 lines of haiku translated to English")] string[] english,
|
||||
[Description("One relevant image name from the valid list")] string? image_name,
|
||||
[Description("CSS Gradient color for the background")] string? gradient)
|
||||
{
|
||||
var newHaiku = new Haiku
|
||||
{
|
||||
Japanese = japanese ?? [],
|
||||
English = english ?? [],
|
||||
ImageName = image_name,
|
||||
Gradient = gradient ?? string.Empty
|
||||
};
|
||||
|
||||
// Add to beginning of list (newest first), removing placeholder if present
|
||||
var updatedHaikus = new List<Haiku> { newHaiku };
|
||||
updatedHaikus.AddRange(haikus.Where(h => h.English.Count == 0 || h.English[0] != "A placeholder verse—"));
|
||||
haikus = updatedHaikus;
|
||||
|
||||
// Reset carousel to show the new haiku
|
||||
InvokeAsync(() =>
|
||||
{
|
||||
carouselRef?.ResetToFirst();
|
||||
StateHasChanged();
|
||||
});
|
||||
|
||||
return newHaiku;
|
||||
}
|
||||
|
||||
private void ResetConversation()
|
||||
{
|
||||
haikus = [Haiku.CreatePlaceholder()];
|
||||
carouselRef?.ResetToFirst();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/* Tool Based Generative UI Demo Layout */
|
||||
.tool-generative-ui-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
height: 100%;
|
||||
background: linear-gradient(170deg, #e9ecef 0%, #ced4da 100%);
|
||||
}
|
||||
|
||||
/* Main Display Area (Carousel) */
|
||||
.main-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Chat Panel */
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-left: 1px solid #e2e8f0;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Override agent suggestions for this demo */
|
||||
::deep .agent-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
::deep .suggestion-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #ff6f61;
|
||||
border-radius: 20px;
|
||||
color: #ff6f61;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
::deep .suggestion-button:hover {
|
||||
background: #ff6f61;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive Layout */
|
||||
@media (max-width: 1024px) {
|
||||
.tool-generative-ui-layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
border-left: none;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.main-display {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tool-generative-ui-layout {
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.main-display {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user