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) { this.pipeline = pipeline; } public ObservableCollection Messages { get; } = []; public ObservableCollection ActivityMessages { get; } = []; public string Draft { get; set; } = ""; public bool IsThinking { get; private set; } public string ActivityMessage { get; private set; } = ThinkingPlaceholder; public event EventHandler? Changed; public async Task SendAsync(CancellationToken cancellationToken = default) { var prompt = Draft.Trim(); if (string.IsNullOrWhiteSpace(prompt) || IsThinking) { return; } Draft = ""; var startedAt = Stopwatch.GetTimestamp(); var activityLines = new List(); var hasVisibleThinking = false; var activityContext = SynchronizationContext.Current; var priorConversation = Messages .Select(item => item.Message) .OfType() .ToList(); Messages.Add(WorkflowRulesEditorConversationItem.ChatMessage( new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, prompt))); ActivityMessages.Clear(); IsThinking = true; ActivityMessage = ThinkingPlaceholder; OnChanged(); try { var result = await pipeline.SendAsync( priorConversation, prompt, cancellationToken, 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) { AddCompletedActivity(startedAt, activityLines, hasVisibleThinking); Messages.Add(WorkflowRulesEditorConversationItem.ChatMessage( new WorkflowRulesEditorChatMessage( WorkflowRulesEditorChatRole.Agent, $"Meeting Summary Agent failed: {exception.Message}"))); } finally { IsThinking = false; ActivityMessages.Clear(); ActivityMessage = ThinkingPlaceholder; OnChanged(); } } private bool ApplyActivityUpdate( SynchronizationContext? context, WorkflowRulesEditorActivityUpdate update, List activityLines) { if (context is null || SynchronizationContext.Current == context) { 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 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); } OnChanged(); return hasVisibleThinking; } private void AddCompletedActivity( long startedAt, IReadOnlyList 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() { Changed?.Invoke(this, EventArgs.Empty); } }