Public Access
Update meeting summary agent UI
This commit is contained in:
@@ -22,6 +22,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
private readonly LiteLlmResponsesCompactionOptions? compactionOptions;
|
||||
private readonly ILogger? logger;
|
||||
private readonly Action? retrying;
|
||||
private readonly Action<string>? reasoningSummaryChanged;
|
||||
private int responsesRequestCount;
|
||||
|
||||
public LiteLlmResponsesChatClient(
|
||||
@@ -35,7 +36,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
LiteLlmResponsesCompactionOptions? compactionOptions = null,
|
||||
ILogger? logger = null,
|
||||
bool firstRequestIsUser = true,
|
||||
Action? retrying = null)
|
||||
Action? retrying = null,
|
||||
Action<string>? reasoningSummaryChanged = null)
|
||||
: this(
|
||||
new HttpClient { BaseAddress = NormalizeEndpoint(endpoint) },
|
||||
apiKey,
|
||||
@@ -47,7 +49,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
compactionOptions,
|
||||
logger,
|
||||
firstRequestIsUser,
|
||||
retrying)
|
||||
retrying,
|
||||
reasoningSummaryChanged)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -62,7 +65,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
LiteLlmResponsesCompactionOptions? compactionOptions = null,
|
||||
ILogger? logger = null,
|
||||
bool firstRequestIsUser = true,
|
||||
Action? retrying = null)
|
||||
Action? retrying = null,
|
||||
Action<string>? reasoningSummaryChanged = null)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
@@ -74,6 +78,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
this.compactionOptions = compactionOptions;
|
||||
this.logger = logger;
|
||||
this.retrying = retrying;
|
||||
this.reasoningSummaryChanged = reasoningSummaryChanged;
|
||||
responsesRequestCount = firstRequestIsUser ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -90,7 +95,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
var payload = await CreateCompactedPayloadAsync(messages.ToList(), options, cancellationToken).ConfigureAwait(false);
|
||||
var responseJson = await PostWithRetryAsync(payload, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = ParseResponseJson(responseJson);
|
||||
var response = ParseResponseJson(responseJson, out var reasoningSummaries);
|
||||
ReportVisibleReasoningSummaries(reasoningSummaries);
|
||||
LogResponseDiagnostics(responseJson, response);
|
||||
ThrowIfResponseHasNoContent(responseJson, response);
|
||||
LogResponseUsage(response.Usage);
|
||||
@@ -122,10 +128,18 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
}
|
||||
|
||||
public static ChatResponse ParseResponseJson(string responseJson)
|
||||
{
|
||||
return ParseResponseJson(responseJson, out _);
|
||||
}
|
||||
|
||||
private static ChatResponse ParseResponseJson(
|
||||
string responseJson,
|
||||
out IReadOnlyList<string> reasoningSummaries)
|
||||
{
|
||||
using var document = JsonDocument.Parse(responseJson);
|
||||
var root = document.RootElement;
|
||||
var contents = new List<AIContent>();
|
||||
reasoningSummaries = ExtractVisibleReasoningSummaries(root);
|
||||
|
||||
if (root.TryGetProperty("output", out var output) && output.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
@@ -162,6 +176,54 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
};
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<string> ExtractVisibleReasoningSummaries(string responseJson)
|
||||
{
|
||||
using var document = JsonDocument.Parse(responseJson);
|
||||
return ExtractVisibleReasoningSummaries(document.RootElement);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ExtractVisibleReasoningSummaries(JsonElement root)
|
||||
{
|
||||
var summaries = new List<string>();
|
||||
|
||||
if (!root.TryGetProperty("output", out var output) || output.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return summaries;
|
||||
}
|
||||
|
||||
foreach (var item in output.EnumerateArray())
|
||||
{
|
||||
if (GetString(item, "type") == "reasoning")
|
||||
{
|
||||
AddVisibleReasoningSummaryText(summaries, item);
|
||||
}
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private void ReportVisibleReasoningSummaries(IReadOnlyList<string> summaries)
|
||||
{
|
||||
if (reasoningSummaryChanged is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var summary in summaries)
|
||||
{
|
||||
try
|
||||
{
|
||||
reasoningSummaryChanged(summary);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger?.LogWarning(
|
||||
exception,
|
||||
"Reasoning summary callback failed; continuing with the LiteLLM response.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JsonObject> CreateCompactedPayloadAsync(
|
||||
IReadOnlyList<ChatMessage> messages,
|
||||
ChatOptions? options,
|
||||
@@ -660,6 +722,66 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddVisibleReasoningSummaryText(List<string> summaries, JsonElement item)
|
||||
{
|
||||
AddVisibleReasoningSummaryText(summaries, item, "summary");
|
||||
AddVisibleReasoningSummaryText(summaries, item, "content");
|
||||
}
|
||||
|
||||
private static void AddVisibleReasoningSummaryText(
|
||||
List<string> summaries,
|
||||
JsonElement item,
|
||||
string propertyName)
|
||||
{
|
||||
if (!item.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
AddNonEmpty(summaries, property.GetString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var summaryItem in property.EnumerateArray())
|
||||
{
|
||||
if (summaryItem.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
AddNonEmpty(summaries, summaryItem.GetString());
|
||||
}
|
||||
else if (summaryItem.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (propertyName == "content" && !IsSummaryContent(summaryItem))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddNonEmpty(summaries, GetString(summaryItem, "text"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSummaryContent(JsonElement item)
|
||||
{
|
||||
var type = GetString(item, "type");
|
||||
return !string.IsNullOrWhiteSpace(type)
|
||||
&& type.Contains("summary", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void AddNonEmpty(List<string> values, string? value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
values.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ParseArguments(string? arguments)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arguments))
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class MeetingTaskbarMenuBuilder
|
||||
{
|
||||
var items = new List<MeetingTaskbarMenuItem>
|
||||
{
|
||||
new("Settings and logs", MeetingTaskbarAction.EditRules)
|
||||
new("Open agent", MeetingTaskbarAction.EditRules)
|
||||
};
|
||||
|
||||
if (microphones is { Count: > 0 })
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user