Update meeting summary agent UI

This commit is contained in:
2026-07-01 10:30:28 +02:00
parent 4787bf8cec
commit 92e359646b
18 changed files with 731 additions and 101 deletions
@@ -10,9 +10,66 @@ public sealed record WorkflowRulesEditorChatMessage(
WorkflowRulesEditorChatRole Role,
string Content);
public sealed record WorkflowRulesEditorChatResult(
string Response,
IReadOnlyList<WorkflowRulesEditorChatMessage> Conversation);
public enum WorkflowRulesEditorConversationItemKind
{
Message,
Activity
}
public sealed record WorkflowRulesEditorConversationItem(
WorkflowRulesEditorConversationItemKind Kind,
WorkflowRulesEditorChatMessage? Message,
string Content,
IReadOnlyList<string>? ActivityLines = null)
{
public bool IsExpanded { get; set; }
public static WorkflowRulesEditorConversationItem ChatMessage(WorkflowRulesEditorChatMessage message)
{
return new WorkflowRulesEditorConversationItem(
WorkflowRulesEditorConversationItemKind.Message,
message,
message.Content);
}
public static WorkflowRulesEditorConversationItem Activity(string content, IReadOnlyList<string> activityLines)
{
return new WorkflowRulesEditorConversationItem(
WorkflowRulesEditorConversationItemKind.Activity,
null,
content,
activityLines);
}
}
public sealed record WorkflowRulesEditorChatResult(string Response);
public enum WorkflowRulesEditorActivityKind
{
Status,
ToolCall,
Thinking
}
public sealed record WorkflowRulesEditorActivityUpdate(
WorkflowRulesEditorActivityKind Kind,
string Text)
{
public static WorkflowRulesEditorActivityUpdate Status(string text)
{
return new WorkflowRulesEditorActivityUpdate(WorkflowRulesEditorActivityKind.Status, text);
}
public static WorkflowRulesEditorActivityUpdate ToolCall(string toolName)
{
return new WorkflowRulesEditorActivityUpdate(WorkflowRulesEditorActivityKind.ToolCall, toolName);
}
public static WorkflowRulesEditorActivityUpdate Thinking(string text)
{
return new WorkflowRulesEditorActivityUpdate(WorkflowRulesEditorActivityKind.Thinking, text);
}
}
public interface IWorkflowRulesEditorChatPipeline
{
@@ -20,5 +77,5 @@ public interface IWorkflowRulesEditorChatPipeline
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<string>? statusChanged = null);
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null);
}
@@ -59,11 +59,11 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<string>? statusChanged = null)
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
{
if (string.IsNullOrWhiteSpace(userMessage))
{
return new WorkflowRulesEditorChatResult("", conversation);
return new WorkflowRulesEditorChatResult("");
}
var agentOptions = options.WorkflowRulesEditor.ToEffectiveAgentOptions(options.Agent);
@@ -108,10 +108,18 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
compactionOptions,
logger,
firstRequestIsUser: true,
retrying: () => statusChanged?.Invoke("Reconnecting..."));
retrying: () => activityChanged?.Invoke(WorkflowRulesEditorActivityUpdate.Status("Reconnecting...")),
reasoningSummaryChanged: text => activityChanged?.Invoke(WorkflowRulesEditorActivityUpdate.Thinking(text)));
var functionClient = chatClient
.AsBuilder()
.UseFunctionInvocation(loggerFactory)
.UseFunctionInvocation(loggerFactory, client =>
{
client.FunctionInvoker = async (context, token) =>
{
activityChanged?.Invoke(WorkflowRulesEditorActivityUpdate.ToolCall(context.Function.Name));
return await context.Function.InvokeAsync(context.Arguments, token);
};
})
.Build();
var response = await functionClient.GetResponseAsync(
@@ -121,11 +129,7 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
var responseText = string.IsNullOrWhiteSpace(response.Text)
? "(No response text returned.)"
: response.Text.Trim();
var nextConversation = conversation
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, userMessage.Trim()))
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.Agent, responseText))
.ToList();
return new WorkflowRulesEditorChatResult(responseText, nextConversation);
return new WorkflowRulesEditorChatResult(responseText);
}
private static ChatMessage ToChatMessage(WorkflowRulesEditorChatMessage message)
@@ -1,9 +1,11 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace MeetingAssistant.Workflow;
public sealed class WorkflowRulesEditorChatViewModel
{
private const string ThinkingPlaceholder = "Thinking...";
private readonly IWorkflowRulesEditorChatPipeline pipeline;
public WorkflowRulesEditorChatViewModel(IWorkflowRulesEditorChatPipeline pipeline)
@@ -11,13 +13,15 @@ public sealed class WorkflowRulesEditorChatViewModel
this.pipeline = pipeline;
}
public ObservableCollection<WorkflowRulesEditorChatMessage> Messages { get; } = [];
public ObservableCollection<WorkflowRulesEditorConversationItem> Messages { get; } = [];
public ObservableCollection<string> ActivityMessages { get; } = [];
public string Draft { get; set; } = "";
public bool IsThinking { get; private set; }
public string ActivityMessage { get; private set; } = "Thinking...";
public string ActivityMessage { get; private set; } = ThinkingPlaceholder;
public event EventHandler? Changed;
@@ -30,10 +34,19 @@ public sealed class WorkflowRulesEditorChatViewModel
}
Draft = "";
var priorConversation = Messages.ToList();
Messages.Add(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, prompt));
var startedAt = Stopwatch.GetTimestamp();
var activityLines = new List<string>();
var hasVisibleThinking = false;
var activityContext = SynchronizationContext.Current;
var priorConversation = Messages
.Select(item => item.Message)
.OfType<WorkflowRulesEditorChatMessage>()
.ToList();
Messages.Add(WorkflowRulesEditorConversationItem.ChatMessage(
new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, prompt)));
ActivityMessages.Clear();
IsThinking = true;
ActivityMessage = "Thinking...";
ActivityMessage = ThinkingPlaceholder;
OnChanged();
try
@@ -42,36 +55,121 @@ public sealed class WorkflowRulesEditorChatViewModel
priorConversation,
prompt,
cancellationToken,
SetActivityMessage);
Messages.Clear();
foreach (var message in result.Conversation)
{
Messages.Add(message);
}
update => hasVisibleThinking |= ApplyActivityUpdate(activityContext, update, activityLines));
AddCompletedActivity(startedAt, activityLines, hasVisibleThinking);
Messages.Add(WorkflowRulesEditorConversationItem.ChatMessage(
new WorkflowRulesEditorChatMessage(
WorkflowRulesEditorChatRole.Agent,
string.IsNullOrWhiteSpace(result.Response)
? "(No response text returned.)"
: result.Response.Trim())));
}
catch (Exception exception) when (exception is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
{
Messages.Add(new WorkflowRulesEditorChatMessage(
WorkflowRulesEditorChatRole.Agent,
$"Settings and logs failed: {exception.Message}"));
AddCompletedActivity(startedAt, activityLines, hasVisibleThinking);
Messages.Add(WorkflowRulesEditorConversationItem.ChatMessage(
new WorkflowRulesEditorChatMessage(
WorkflowRulesEditorChatRole.Agent,
$"Meeting Summary Agent failed: {exception.Message}")));
}
finally
{
IsThinking = false;
ActivityMessage = "Thinking...";
ActivityMessages.Clear();
ActivityMessage = ThinkingPlaceholder;
OnChanged();
}
}
private void SetActivityMessage(string message)
private bool ApplyActivityUpdate(
SynchronizationContext? context,
WorkflowRulesEditorActivityUpdate update,
List<string> activityLines)
{
if (!IsThinking || string.IsNullOrWhiteSpace(message))
if (context is null || SynchronizationContext.Current == context)
{
return;
return ApplyActivityUpdate(update, activityLines);
}
var hasVisibleThinking = false;
Exception? exception = null;
context.Send(
_ =>
{
try
{
hasVisibleThinking = ApplyActivityUpdate(update, activityLines);
}
catch (Exception caught)
{
exception = caught;
}
},
null);
if (exception is not null)
{
throw exception;
}
return hasVisibleThinking;
}
private bool ApplyActivityUpdate(
WorkflowRulesEditorActivityUpdate update,
List<string> activityLines)
{
if (!IsThinking || string.IsNullOrWhiteSpace(update.Text))
{
return false;
}
var hasVisibleThinking = false;
if (update.Kind == WorkflowRulesEditorActivityKind.ToolCall)
{
var line = $"Called tool: {update.Text.Trim()}";
ActivityMessages.Add(line);
activityLines.Add(line);
}
else if (update.Kind == WorkflowRulesEditorActivityKind.Thinking)
{
var line = update.Text.Trim();
ActivityMessages.Add(line);
activityLines.Add(line);
hasVisibleThinking = true;
}
else
{
var line = update.Text.Trim();
ActivityMessage = line;
activityLines.Add(line);
}
ActivityMessage = message;
OnChanged();
return hasVisibleThinking;
}
private void AddCompletedActivity(
long startedAt,
IReadOnlyList<string> activityLines,
bool hasVisibleThinking)
{
var completedActivityLines = hasVisibleThinking
? activityLines.ToArray()
: new[] { ThinkingPlaceholder }.Concat(activityLines).ToArray();
Messages.Add(WorkflowRulesEditorConversationItem.Activity(
$"Worked for {FormatDuration(Stopwatch.GetElapsedTime(startedAt))}",
completedActivityLines));
}
private static string FormatDuration(TimeSpan duration)
{
if (duration.TotalMinutes >= 1)
{
return $"{(int)duration.TotalMinutes}m {duration.Seconds}s";
}
return $"{Math.Max(0, (int)Math.Round(duration.TotalSeconds))}s";
}
private void OnChanged()
@@ -11,7 +11,7 @@ namespace MeetingAssistant.Workflow;
internal sealed class WpfWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService
{
internal const string WindowTitle = "Settings and logs";
internal const string WindowTitle = "Meeting Summary Agent";
private readonly IServiceProvider services;
private readonly WorkflowRulesEditorMarkdownLinkResolver linkResolver;
@@ -288,19 +288,21 @@ internal sealed class WorkflowRulesEditorWpfWindow : Window
conversationPanel.ActualHeight);
conversationPanel.Children.Clear();
foreach (var message in viewModel.Messages)
foreach (var item in viewModel.Messages)
{
conversationPanel.Children.Add(CreateMessageCard(message));
conversationPanel.Children.Add(item.Kind == WorkflowRulesEditorConversationItemKind.Activity
? CreateActivityExpander(item)
: CreateMessageCard(item.Message!));
}
if (viewModel.IsThinking)
{
conversationPanel.Children.Add(new TextBlock
foreach (var activity in viewModel.ActivityMessages)
{
Text = viewModel.ActivityMessage,
Foreground = MutedText,
Margin = new Thickness(4, 2, 4, 2)
});
conversationPanel.Children.Add(CreateActivityLine(activity));
}
conversationPanel.Children.Add(CreateActivityLine(viewModel.ActivityMessage));
}
sendButton.IsEnabled = !viewModel.IsThinking;
@@ -338,6 +340,43 @@ internal sealed class WorkflowRulesEditorWpfWindow : Window
return card;
}
private static Expander CreateActivityExpander(WorkflowRulesEditorConversationItem item)
{
var details = new StackPanel
{
Orientation = Orientation.Vertical,
Margin = new Thickness(16, 2, 4, 6)
};
foreach (var line in item.ActivityLines ?? [])
{
details.Children.Add(CreateActivityLine(line));
}
var expander = new Expander
{
Header = item.Content,
Content = details,
IsExpanded = item.IsExpanded,
Foreground = MutedText,
Background = Brushes.Transparent,
Margin = new Thickness(4, 0, 4, 8)
};
expander.Expanded += (_, _) => item.IsExpanded = true;
expander.Collapsed += (_, _) => item.IsExpanded = false;
return expander;
}
private static TextBlock CreateActivityLine(string text)
{
return new TextBlock
{
Text = text,
Foreground = MutedText,
Margin = new Thickness(4, 2, 4, 2)
};
}
private static Style CreateSendButtonStyle()
{
var style = new Style(typeof(Button));