mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Improve shared state
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Default template for rendering DataContent. Renders nothing by default
|
||||
/// as DataContent typically contains binary/JSON data not meant for display.
|
||||
/// </summary>
|
||||
public class DataContentTemplate : ContentTemplateBase
|
||||
{
|
||||
[CascadingParameter] internal MessageListContext Context { get; set; } = default!;
|
||||
|
||||
public override void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
this.ChildContent = this.RenderData;
|
||||
}
|
||||
|
||||
public override Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
this.Context.RegisterContentTemplate(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override bool When(ContentContext context)
|
||||
{
|
||||
return context.Content is DataContent;
|
||||
}
|
||||
|
||||
private RenderFragment RenderData(ContentContext content) => builder =>
|
||||
{
|
||||
// By default, render nothing.
|
||||
};
|
||||
}
|
||||
|
||||
+83
-79
@@ -15,95 +15,99 @@
|
||||
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 class="recipe-panel-container">
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<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 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>
|
||||
<button class="remove-button" @onclick="@(() => RemoveIngredient(index))">×</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions-section">
|
||||
<div class="section-header">
|
||||
<h2>Instructions</h2>
|
||||
<button class="add-button" @onclick="AddInstruction">+ Add Step</button>
|
||||
<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>
|
||||
<div class="instruction-input-wrapper">
|
||||
<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>
|
||||
</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>
|
||||
<button class="improve-button" @onclick="ImproveWithAI" disabled="@isProcessing">
|
||||
@(isProcessing ? "Please Wait..." : "Improve with AI")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-panel">
|
||||
|
||||
+182
-84
@@ -2,37 +2,45 @@
|
||||
.shared-state-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
gap: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Recipe Panel */
|
||||
.recipe-panel {
|
||||
/* Recipe Panel Container - centers the card */
|
||||
.recipe-panel-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Recipe Panel - the actual card */
|
||||
.recipe-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* Recipe Header */
|
||||
.recipe-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
font-size: 24px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding: 8px 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
width: 100%;
|
||||
@@ -40,12 +48,15 @@
|
||||
|
||||
.recipe-title:focus {
|
||||
outline: none;
|
||||
border-bottom-color: #3b82f6;
|
||||
}
|
||||
|
||||
.recipe-title::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.recipe-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
@@ -59,18 +70,18 @@
|
||||
}
|
||||
|
||||
.meta-item select {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
appearance: auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.meta-item select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
@@ -89,153 +100,242 @@
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #f97316;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* Dietary Preferences */
|
||||
.preferences-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, auto);
|
||||
gap: 8px 32px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preference-item input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: #3b82f6;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Ingredients */
|
||||
.ingredients-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ingredient-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background-color: white;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 10px;
|
||||
min-width: 0;
|
||||
flex: 1 1 calc(33.333% - 8px);
|
||||
max-width: calc(33.333% - 8px);
|
||||
position: relative;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.ingredient-row:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Hide ingredient remove button by default, show on hover */
|
||||
.ingredient-row .remove-button {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.ingredient-row:hover .remove-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ingredient-icon {
|
||||
font-size: 20px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background-color: #fef3e2;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ingredient-inputs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ingredient-inputs input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ingredient-inputs input:first-child {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.ingredient-inputs input:last-child {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.ingredient-inputs input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.ingredient-inputs input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
.instructions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 0;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* Vertical connecting line */
|
||||
.instructions-list::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 23px;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
width: 2px;
|
||||
border-left: 2px dashed #fdba74;
|
||||
}
|
||||
|
||||
.instruction-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #3b82f6;
|
||||
background-color: #f97316;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.instruction-row input {
|
||||
/* Instruction input wrapper for positioning delete button inside */
|
||||
.instruction-input-wrapper {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.instruction-input-wrapper input,
|
||||
.instruction-input-wrapper textarea {
|
||||
width: 100%;
|
||||
padding: 8px 32px 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
resize: vertical;
|
||||
min-height: 40px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.instruction-row input:focus {
|
||||
.instruction-input-wrapper input:focus,
|
||||
.instruction-input-wrapper textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
/* Instruction remove button - hidden by default, shown on hover, positioned inside textarea */
|
||||
.instruction-input-wrapper .remove-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.instruction-input-wrapper:hover .remove-button,
|
||||
.instruction-input-wrapper:focus-within .remove-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.add-button {
|
||||
padding: 6px 12px;
|
||||
background-color: #e5e7eb;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
background-color: transparent;
|
||||
border: 1px solid #f97316;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
color: #f97316;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
background-color: #d1d5db;
|
||||
background-color: #fff7ed;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
@@ -244,14 +344,12 @@
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
background-color: #fef2f2;
|
||||
border-color: #fca5a5;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.improve-button {
|
||||
padding: 12px 24px;
|
||||
background-color: #3b82f6;
|
||||
padding: 14px 32px;
|
||||
background-color: #f97316;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
@@ -259,31 +357,31 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
margin-top: auto;
|
||||
align-self: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.improve-button:hover:not(:disabled) {
|
||||
background-color: #2563eb;
|
||||
background-color: #ea580c;
|
||||
}
|
||||
|
||||
.improve-button:disabled {
|
||||
background-color: #93c5fd;
|
||||
background-color: #fdba74;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Chat Panel */
|
||||
.chat-panel {
|
||||
width: 400px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
border-left: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user