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:
+14
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.PredictiveStateUpdates;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of the confirm_changes frontend tool.
|
||||
/// </summary>
|
||||
public sealed class ConfirmChangesResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user confirmed the changes.
|
||||
/// </summary>
|
||||
public bool Confirmed { get; set; }
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AGUIDojoClient.Components.Demos.PredictiveStateUpdates;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the document state for the Predictive State Updates demo.
|
||||
/// This model mirrors the server-side DocumentState and is updated via streaming state updates.
|
||||
/// </summary>
|
||||
public sealed class DocumentState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the document content in Markdown format.
|
||||
/// </summary>
|
||||
[JsonPropertyName("document")]
|
||||
public string Document { get; set; } = string.Empty;
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
@* 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
|
||||
@using System.ComponentModel
|
||||
@using Microsoft.Extensions.Logging
|
||||
@implements IDisposable
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject ILogger<PredictiveStateUpdatesDemo> Logger
|
||||
|
||||
<PageTitle>AI Document Editor</PageTitle>
|
||||
|
||||
<div class="predictive-state-layout">
|
||||
@* Left panel: Document editor *@
|
||||
<div class="document-panel">
|
||||
<div class="document-header">
|
||||
<h2>Document Editor</h2>
|
||||
@if (isStreaming)
|
||||
{
|
||||
<span class="streaming-indicator">
|
||||
<span class="streaming-dot"></span>
|
||||
Writing...
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="document-content">
|
||||
@if (string.IsNullOrWhiteSpace(currentDocument))
|
||||
{
|
||||
<div class="document-placeholder">
|
||||
Write whatever you want here in Markdown format...
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<pre class="document-text">@currentDocument</pre>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Right panel: Chat sidebar *@
|
||||
<div class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">AI Document Editor</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>
|
||||
<TextTemplate />
|
||||
</ContentTemplates>
|
||||
</Messages>
|
||||
</div>
|
||||
|
||||
<AgentState TState="DocumentState"
|
||||
CurrentState="@currentDocumentState"
|
||||
OnSnapshot="@DeserializeDocumentState"
|
||||
CurrentStateChanged="@OnDocumentStateChanged" />
|
||||
|
||||
<div class="chat-input-container">
|
||||
<AgentSuggestions Suggestions="@suggestions" />
|
||||
<AgentInput Placeholder="Ask the AI to write or edit..." />
|
||||
</div>
|
||||
</AgentBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Confirmation modal *@
|
||||
@if (awaitingConfirmation)
|
||||
{
|
||||
<div class="confirmation-overlay">
|
||||
<div class="confirmation-modal">
|
||||
<h3>Confirm Changes</h3>
|
||||
<p>Do you want to accept the changes?</p>
|
||||
<div class="confirmation-actions">
|
||||
<button class="confirm-button reject" @onclick="RejectChanges">
|
||||
Reject
|
||||
</button>
|
||||
<button class="confirm-button accept" @onclick="ConfirmChanges">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private AIAgent? agent;
|
||||
private IAgentBoundaryContext? boundaryContext;
|
||||
private ResponseUpdateSubscription? responseSubscription;
|
||||
|
||||
private DocumentState? currentDocumentState;
|
||||
private string currentDocument = string.Empty;
|
||||
private string previousDocument = string.Empty;
|
||||
private bool isStreaming;
|
||||
private bool awaitingConfirmation;
|
||||
|
||||
private Suggestion[] suggestions = [
|
||||
new Suggestion("Write a pirate story", new ChatMessage(ChatRole.User, "Please write a story about a pirate named Candy Beard")),
|
||||
new Suggestion("Write a mermaid story", new ChatMessage(ChatRole.User, "Please write a story about a mermaid named Pearl")),
|
||||
new Suggestion("Add character", new ChatMessage(ChatRole.User, "Add a new character to the story"))
|
||||
];
|
||||
|
||||
[Parameter]
|
||||
public string ScenarioId { get; set; } = "predictive_state_updates";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
agent = ServiceProvider.GetRequiredKeyedService<AIAgent>("predictive-state-updates");
|
||||
}
|
||||
|
||||
private void OnContextCreated(IAgentBoundaryContext context)
|
||||
{
|
||||
boundaryContext = context;
|
||||
|
||||
// Register the confirm_changes frontend tool
|
||||
var confirmChangesTool = AIFunctionFactory.Create(
|
||||
() => ConfirmChangesAsync(context),
|
||||
"confirm_changes",
|
||||
"Ask the user to confirm or reject the document changes.");
|
||||
|
||||
context.RegisterTools(confirmChangesTool);
|
||||
|
||||
// Subscribe to response updates to detect when streaming starts/stops
|
||||
responseSubscription = context.SubscribeToResponseUpdates(OnResponseUpdate);
|
||||
}
|
||||
|
||||
private void OnResponseUpdate()
|
||||
{
|
||||
var update = boundaryContext?.CurrentUpdate;
|
||||
if (update is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for function calls to detect confirm_changes
|
||||
if (update.Contents is not null)
|
||||
{
|
||||
foreach (var content in update.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent call &&
|
||||
string.Equals(call.Name, "confirm_changes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Show confirmation dialog when confirm_changes is called
|
||||
Logger.LogInformation("Detected confirm_changes call, showing confirmation dialog");
|
||||
awaitingConfirmation = true;
|
||||
isStreaming = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private DocumentState? DeserializeDocumentState(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<DocumentState>(data.Span);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDocumentStateChanged(DocumentState? state)
|
||||
{
|
||||
if (state is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentDocumentState = state;
|
||||
|
||||
// Store the previous document before updating (for reject functionality)
|
||||
if (string.IsNullOrEmpty(previousDocument) && !string.IsNullOrEmpty(currentDocument))
|
||||
{
|
||||
previousDocument = currentDocument;
|
||||
}
|
||||
|
||||
// Check if we're starting to stream (document changed)
|
||||
if (currentDocument != state.Document)
|
||||
{
|
||||
isStreaming = true;
|
||||
}
|
||||
|
||||
currentDocument = state.Document;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frontend tool that waits for user confirmation via the UI.
|
||||
/// </summary>
|
||||
[Description("Ask the user to confirm or reject the document changes.")]
|
||||
private static async Task<ConfirmChangesResult> ConfirmChangesAsync(IAgentBoundaryContext context)
|
||||
{
|
||||
// Wait for the user to click Confirm or Reject
|
||||
var response = await context.WaitForResponse("confirm_changes");
|
||||
return (ConfirmChangesResult)response;
|
||||
}
|
||||
|
||||
private void ConfirmChanges()
|
||||
{
|
||||
awaitingConfirmation = false;
|
||||
isStreaming = false;
|
||||
|
||||
// Update the previous document to the current one (changes accepted)
|
||||
previousDocument = currentDocument;
|
||||
|
||||
boundaryContext?.ProvideResponse("confirm_changes", new ConfirmChangesResult { Confirmed = true });
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void RejectChanges()
|
||||
{
|
||||
awaitingConfirmation = false;
|
||||
isStreaming = false;
|
||||
|
||||
// Revert to the previous document
|
||||
currentDocument = previousDocument;
|
||||
|
||||
boundaryContext?.ProvideResponse("confirm_changes", new ConfirmChangesResult { Confirmed = false });
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ResetConversation()
|
||||
{
|
||||
currentDocument = string.Empty;
|
||||
previousDocument = string.Empty;
|
||||
currentDocumentState = null;
|
||||
isStreaming = false;
|
||||
awaitingConfirmation = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
responseSubscription?.Dispose();
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
/* Predictive State Updates Demo Styles */
|
||||
|
||||
/* Main layout - two column */
|
||||
.predictive-state-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Document Panel (Left) */
|
||||
.document-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.document-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.document-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.streaming-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #6366f1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.streaming-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.document-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.document-placeholder {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.document-text {
|
||||
margin: 0;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Chat Panel (Right) */
|
||||
.chat-panel {
|
||||
width: 400px;
|
||||
min-width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.new-chat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.new-chat-button:hover {
|
||||
background: #e5e7eb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* Confirmation Modal */
|
||||
.confirmation-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.confirmation-modal {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px 32px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
min-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirmation-modal h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.confirmation-modal p {
|
||||
margin: 0 0 24px 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.confirmation-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.confirm-button.reject {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.confirm-button.reject:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.confirm-button.accept {
|
||||
background: #6366f1;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-button.accept:hover {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
/* Agent Suggestions styling overrides */
|
||||
::deep .suggestions-container {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
::deep .suggestion-chip {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 8px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
::deep .suggestion-chip:hover {
|
||||
background: #e5e7eb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
Reference in New Issue
Block a user