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:
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.SharedState;
|
||||
|
||||
public sealed class Ingredient
|
||||
{
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public string Amount { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.SharedState;
|
||||
|
||||
public sealed class Recipe
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cooking_time")]
|
||||
public string CookingTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("special_preferences")]
|
||||
public List<string> SpecialPreferences { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<Ingredient> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("instructions")]
|
||||
public List<string> Instructions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.SharedState;
|
||||
|
||||
public sealed class RecipeResponse
|
||||
{
|
||||
[JsonPropertyName("recipe")]
|
||||
public Recipe Recipe { get; set; } = new();
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
@* 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.Text.Json
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
<PageTitle>Shared State</PageTitle>
|
||||
|
||||
<div class="shared-state-layout">
|
||||
<AgentBoundary Agent="@agent" OnContextCreated="@OnContextCreated">
|
||||
<AgentState TState="Recipe"
|
||||
CurrentState="@currentRecipe"
|
||||
OnSnapshot="@DeserializeRecipe"
|
||||
CurrentStateChanged="@OnRecipeChanged">
|
||||
<div class="recipe-panel">
|
||||
<div class="recipe-header">
|
||||
<input type="text" class="recipe-title" @bind="currentRecipe.Title" @bind:event="oninput" placeholder="Recipe name" />
|
||||
<div class="recipe-meta">
|
||||
<div class="meta-item">
|
||||
<span class="meta-icon">🕒</span>
|
||||
<select @bind="currentRecipe.CookingTime">
|
||||
<option value="5 min">5 min</option>
|
||||
<option value="15 min">15 min</option>
|
||||
<option value="30 min">30 min</option>
|
||||
<option value="45 min">45 min</option>
|
||||
<option value="60+ min">60+ min</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-icon">🏆</span>
|
||||
<select @bind="currentRecipe.SkillLevel">
|
||||
<option value="Beginner">Beginner</option>
|
||||
<option value="Intermediate">Intermediate</option>
|
||||
<option value="Advanced">Advanced</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preferences-section">
|
||||
<h2>Dietary Preferences</h2>
|
||||
<div class="preferences-grid">
|
||||
@foreach (var pref in dietaryPreferences)
|
||||
{
|
||||
<label class="preference-item">
|
||||
<input type="checkbox"
|
||||
checked="@currentRecipe.SpecialPreferences.Contains(pref)"
|
||||
@onchange="@(e => TogglePreference(pref, (bool?)e.Value ?? false))" />
|
||||
<span>@pref</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ingredients-section">
|
||||
<div class="section-header">
|
||||
<h2>Ingredients</h2>
|
||||
<button class="add-button" @onclick="AddIngredient">+ Add Ingredient</button>
|
||||
</div>
|
||||
<div class="ingredients-list">
|
||||
@for (int i = 0; i < currentRecipe.Ingredients.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var ingredient = currentRecipe.Ingredients[index];
|
||||
<div class="ingredient-row">
|
||||
<span class="ingredient-icon">@ingredient.Icon</span>
|
||||
<div class="ingredient-inputs">
|
||||
<input type="text" placeholder="Ingredient name"
|
||||
value="@ingredient.Name"
|
||||
@onchange="@(e => UpdateIngredientName(index, e.Value?.ToString() ?? ""))" />
|
||||
<input type="text" placeholder="Amount"
|
||||
value="@ingredient.Amount"
|
||||
@onchange="@(e => UpdateIngredientAmount(index, e.Value?.ToString() ?? ""))" />
|
||||
</div>
|
||||
<button class="remove-button" @onclick="@(() => RemoveIngredient(index))">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions-section">
|
||||
<div class="section-header">
|
||||
<h2>Instructions</h2>
|
||||
<button class="add-button" @onclick="AddInstruction">+ Add Step</button>
|
||||
</div>
|
||||
<div class="instructions-list">
|
||||
@for (int i = 0; i < currentRecipe.Instructions.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
<div class="instruction-row">
|
||||
<span class="step-number">@(index + 1)</span>
|
||||
<input type="text"
|
||||
value="@currentRecipe.Instructions[index]"
|
||||
@onchange="@(e => UpdateInstruction(index, e.Value?.ToString() ?? ""))" />
|
||||
<button class="remove-button" @onclick="@(() => RemoveInstruction(index))">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="improve-button" @onclick="ImproveWithAI" disabled="@isProcessing">
|
||||
@(isProcessing ? "Please Wait..." : "Improve with AI")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<span class="chat-title">AI Recipe Assistant</span>
|
||||
</div>
|
||||
<div class="chat-messages">
|
||||
<Messages>
|
||||
<ContentTemplates>
|
||||
<TextTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
</div>
|
||||
<div class="chat-input-container">
|
||||
<AgentInput Placeholder="Ask about your recipe..." />
|
||||
</div>
|
||||
</div>
|
||||
</AgentState>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private IAgentBoundaryContext? boundaryContext;
|
||||
private Recipe currentRecipe = CreateDefaultRecipe();
|
||||
private bool isProcessing;
|
||||
|
||||
private static readonly string[] dietaryPreferences = [
|
||||
"High Protein", "Low Carb", "Spicy", "Budget-Friendly",
|
||||
"One-Pot Meal", "Vegetarian", "Vegan"
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "shared_state";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("shared-state");
|
||||
}
|
||||
|
||||
private void OnContextCreated(IAgentBoundaryContext context)
|
||||
{
|
||||
boundaryContext = context;
|
||||
}
|
||||
|
||||
private static Recipe CreateDefaultRecipe() => new()
|
||||
{
|
||||
Title = "Make Your Recipe",
|
||||
SkillLevel = "Intermediate",
|
||||
CookingTime = "45 min",
|
||||
SpecialPreferences = [],
|
||||
Ingredients =
|
||||
[
|
||||
new Ingredient { Icon = "🥕", Name = "Carrots", Amount = "3 large, grated" },
|
||||
new Ingredient { Icon = "🌾", Name = "All-Purpose Flour", Amount = "2 cups" }
|
||||
],
|
||||
Instructions =
|
||||
[
|
||||
"Preheat oven to 350°F (175°C)"
|
||||
]
|
||||
};
|
||||
|
||||
private Recipe? DeserializeRecipe(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = JsonSerializer.Deserialize<RecipeResponse>(data.Span);
|
||||
return response?.Recipe;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRecipeChanged(Recipe? recipe)
|
||||
{
|
||||
if (recipe is not null)
|
||||
{
|
||||
currentRecipe = recipe;
|
||||
isProcessing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ImproveWithAI()
|
||||
{
|
||||
if (boundaryContext is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Serialize current recipe state with wrapper
|
||||
var stateWrapper = new { recipe = currentRecipe };
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(stateWrapper, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
|
||||
// Create message with state attached as DataContent
|
||||
var message = new ChatMessage(ChatRole.User,
|
||||
[
|
||||
new TextContent("Improve the recipe"),
|
||||
new DataContent(stateBytes, "application/json")
|
||||
]);
|
||||
|
||||
await boundaryContext.SendAsync(message);
|
||||
}
|
||||
|
||||
private void TogglePreference(string preference, bool isChecked)
|
||||
{
|
||||
if (isChecked && !currentRecipe.SpecialPreferences.Contains(preference))
|
||||
{
|
||||
currentRecipe.SpecialPreferences.Add(preference);
|
||||
}
|
||||
else if (!isChecked)
|
||||
{
|
||||
currentRecipe.SpecialPreferences.Remove(preference);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddIngredient()
|
||||
{
|
||||
currentRecipe.Ingredients.Add(new Ingredient { Icon = "🥄", Name = "", Amount = "" });
|
||||
}
|
||||
|
||||
private void RemoveIngredient(int index)
|
||||
{
|
||||
if (index >= 0 && index < currentRecipe.Ingredients.Count)
|
||||
{
|
||||
currentRecipe.Ingredients.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIngredientName(int index, string name)
|
||||
{
|
||||
if (index >= 0 && index < currentRecipe.Ingredients.Count)
|
||||
{
|
||||
currentRecipe.Ingredients[index].Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIngredientAmount(int index, string amount)
|
||||
{
|
||||
if (index >= 0 && index < currentRecipe.Ingredients.Count)
|
||||
{
|
||||
currentRecipe.Ingredients[index].Amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInstruction()
|
||||
{
|
||||
currentRecipe.Instructions.Add("");
|
||||
}
|
||||
|
||||
private void RemoveInstruction(int index)
|
||||
{
|
||||
if (index >= 0 && index < currentRecipe.Instructions.Count)
|
||||
{
|
||||
currentRecipe.Instructions.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateInstruction(int index, string instruction)
|
||||
{
|
||||
if (index >= 0 && index < currentRecipe.Instructions.Count)
|
||||
{
|
||||
currentRecipe.Instructions[index] = instruction;
|
||||
}
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
/* Shared State Demo Layout */
|
||||
.shared-state-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Recipe Panel */
|
||||
.recipe-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Recipe Header */
|
||||
.recipe-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding: 8px 0;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recipe-title:focus {
|
||||
outline: none;
|
||||
border-bottom-color: #3b82f6;
|
||||
}
|
||||
|
||||
.recipe-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.meta-item select {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.meta-item select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.preferences-section,
|
||||
.ingredients-section,
|
||||
.instructions-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preferences-section h2,
|
||||
.ingredients-section h2,
|
||||
.instructions-section h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Dietary Preferences */
|
||||
.preferences-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preference-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.preference-item:hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.preference-item input[type="checkbox"] {
|
||||
accent-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Ingredients */
|
||||
.ingredients-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ingredient-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ingredient-icon {
|
||||
font-size: 20px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ingredient-inputs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ingredient-inputs input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.ingredient-inputs input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
.instructions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.instruction-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.instruction-row input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.instruction-row input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.add-button {
|
||||
padding: 6px 12px;
|
||||
background-color: #e5e7eb;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
background-color: #d1d5db;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
background-color: #fef2f2;
|
||||
border-color: #fca5a5;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.improve-button {
|
||||
padding: 12px 24px;
|
||||
background-color: #3b82f6;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.improve-button:hover:not(:disabled) {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.improve-button:disabled {
|
||||
background-color: #93c5fd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Chat Panel */
|
||||
.chat-panel {
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
Reference in New Issue
Block a user