#if WINDOWS using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using static MeetingAssistant.Workflow.WorkflowRulesEditorWpfTheme; namespace MeetingAssistant.Workflow; internal sealed class WpfWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService { internal const string WindowTitle = "Meeting Summary Agent"; private readonly IServiceProvider services; private readonly WorkflowRulesEditorMarkdownLinkResolver linkResolver; private readonly ILogger logger; private readonly object sync = new(); private Dispatcher? dispatcher; private WorkflowRulesEditorWpfWindow? window; private bool isStarted; public WpfWorkflowRulesEditorWindowService( IServiceProvider services, WorkflowRulesEditorMarkdownLinkResolver linkResolver, ILogger logger) { this.services = services; this.linkResolver = linkResolver; this.logger = logger; } public void Show() { EnsureUiThread(); dispatcher?.BeginInvoke(ShowOnUiThread); } private void EnsureUiThread() { lock (sync) { if (isStarted) { return; } isStarted = true; var ready = new ManualResetEventSlim(); var thread = new Thread(() => RunApplication(ready)) { IsBackground = true, Name = "Meeting Assistant Settings and Logs" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); if (!ready.Wait(TimeSpan.FromSeconds(10))) { isStarted = false; throw new TimeoutException("Settings and logs UI thread did not start."); } } } private void RunApplication(ManualResetEventSlim ready) { try { logger.LogInformation("Settings and logs WPF thread started"); var application = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; dispatcher = application.Dispatcher; ready.Set(); application.Run(); } catch (Exception exception) { logger.LogError(exception, "Settings and logs WPF thread failed"); ready.Set(); } finally { lock (sync) { dispatcher = null; window = null; isStarted = false; } } } private void ShowOnUiThread() { try { window ??= new WorkflowRulesEditorWpfWindow( services.GetRequiredService(), linkResolver, () => window = null); window.Show(); if (window.WindowState == WindowState.Minimized) { window.WindowState = WindowState.Normal; } window.Activate(); } catch (Exception exception) { logger.LogError(exception, "Settings and logs WPF window failed"); } } } internal sealed class WorkflowRulesEditorWpfWindow : Window { private readonly WorkflowRulesEditorChatViewModel viewModel; private readonly WorkflowRulesEditorMarkdownLinkResolver linkResolver; private readonly Action closed; private readonly StackPanel conversationPanel = new() { Orientation = Orientation.Vertical }; private readonly ScrollViewer conversationScroll = new(); private readonly Border inputFrame = new(); private readonly TextBox input = new(); private readonly TextBlock inputPlaceholder = new(); private readonly Button sendButton = new(); private readonly EventHandler changedHandler; public WorkflowRulesEditorWpfWindow( WorkflowRulesEditorChatViewModel viewModel, WorkflowRulesEditorMarkdownLinkResolver linkResolver, Action closed) { this.viewModel = viewModel; this.linkResolver = linkResolver; this.closed = closed; changedHandler = (_, _) => RequestRender(); this.viewModel.Changed += changedHandler; Title = WpfWorkflowRulesEditorWindowService.WindowTitle; Width = 840; Height = 760; MinWidth = 520; MinHeight = 460; Background = WindowBackground; Content = CreateLayout(); var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "meeting-assistant.ico"); if (File.Exists(iconPath)) { Icon = System.Windows.Media.Imaging.BitmapFrame.Create(new Uri(iconPath)); } Closed += (_, _) => { viewModel.Changed -= changedHandler; closed(); }; ConfigureInput(); Render(); } private UIElement CreateLayout() { var root = new Grid { Margin = new Thickness(12) }; root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); conversationScroll.Content = conversationPanel; conversationScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; conversationScroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; conversationScroll.Padding = new Thickness(0, 0, 4, 0); Grid.SetRow(conversationScroll, 0); root.Children.Add(conversationScroll); var inputRow = new Grid { Margin = new Thickness(0, 10, 0, 0) }; inputRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); inputRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); Grid.SetRow(inputRow, 1); Grid.SetColumn(inputFrame, 0); inputRow.Children.Add(inputFrame); Grid.SetColumn(sendButton, 1); inputRow.Children.Add(sendButton); root.Children.Add(inputRow); return root; } private void ConfigureInput() { input.AcceptsReturn = true; input.TextWrapping = TextWrapping.Wrap; input.MinHeight = 86; input.MaxHeight = 124; input.Padding = new Thickness(12, 9, 12, 9); input.Background = Brushes.Transparent; input.Foreground = PrimaryText; input.BorderThickness = new Thickness(0); input.CaretBrush = PrimaryText; input.FontSize = 14; input.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; input.TextChanged += (_, _) => { viewModel.Draft = input.Text; inputPlaceholder.Visibility = string.IsNullOrEmpty(input.Text) ? Visibility.Visible : Visibility.Collapsed; }; input.PreviewKeyDown += OnInputPreviewKeyDown; input.GotKeyboardFocus += (_, _) => inputFrame.BorderBrush = InputFocusedBorder; input.LostKeyboardFocus += (_, _) => inputFrame.BorderBrush = InputBorder; inputPlaceholder.Text = "ask me what I can do for you"; inputPlaceholder.Foreground = MutedText; inputPlaceholder.Margin = new Thickness(13, 9, 13, 9); inputPlaceholder.IsHitTestVisible = false; var inputGrid = new Grid(); inputGrid.Children.Add(input); inputGrid.Children.Add(inputPlaceholder); inputFrame.Child = inputGrid; inputFrame.Margin = new Thickness(0, 0, 10, 0); inputFrame.Background = InputBackground; inputFrame.BorderBrush = InputBorder; inputFrame.BorderThickness = new Thickness(1); inputFrame.CornerRadius = new CornerRadius(7); sendButton.Content = "Send"; sendButton.Width = 104; sendButton.MinHeight = 86; sendButton.Background = ButtonBackground; sendButton.Foreground = PrimaryText; sendButton.BorderBrush = InputBorder; sendButton.BorderThickness = new Thickness(1); sendButton.FontWeight = FontWeights.SemiBold; sendButton.Cursor = Cursors.Hand; sendButton.Style = CreateSendButtonStyle(); sendButton.Click += async (_, _) => await SendAsync(); } private async void OnInputPreviewKeyDown(object sender, KeyEventArgs args) { if (IsEnterKey(args) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) is false) { args.Handled = true; await SendAsync(); } } private static bool IsEnterKey(KeyEventArgs args) { return args.Key is Key.Enter or Key.Return || args.SystemKey is Key.Enter or Key.Return || args.ImeProcessedKey is Key.Enter or Key.Return; } private async Task SendAsync() { var sendTask = viewModel.SendAsync(); input.Text = viewModel.Draft; await sendTask; } private void RequestRender() { if (Dispatcher.CheckAccess()) { Render(); return; } Dispatcher.BeginInvoke(Render); } private void Render() { var shouldAutoScroll = WorkflowRulesEditorScrollPolicy.ShouldAutoScroll( conversationScroll.VerticalOffset, conversationScroll.ViewportHeight, conversationPanel.ActualHeight); conversationPanel.Children.Clear(); foreach (var item in viewModel.Messages) { conversationPanel.Children.Add(item.Kind == WorkflowRulesEditorConversationItemKind.Activity ? CreateActivityExpander(item) : CreateMessageCard(item.Message!)); } if (viewModel.IsThinking) { foreach (var activity in viewModel.ActivityMessages) { conversationPanel.Children.Add(CreateActivityLine(activity)); } conversationPanel.Children.Add(CreateActivityLine(viewModel.ActivityMessage)); } sendButton.IsEnabled = !viewModel.IsThinking; sendButton.Cursor = sendButton.IsEnabled ? Cursors.Hand : Cursors.Arrow; if (shouldAutoScroll) { Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, ScrollConversationToBottom); Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, ScrollConversationToBottom); } } private void ScrollConversationToBottom() { conversationScroll.ScrollToVerticalOffset(WorkflowRulesEditorScrollPolicy.GetBottomOffset( conversationScroll.ViewportHeight, conversationPanel.ActualHeight)); } private UIElement CreateMessageCard(WorkflowRulesEditorChatMessage message) { var isUser = message.Role == WorkflowRulesEditorChatRole.User; var cardContent = new StackPanel { Orientation = Orientation.Vertical }; cardContent.Children.Add(WorkflowRulesEditorMarkdownRenderer.CreateContent(message.Content, linkResolver)); var card = new Border { Padding = new Thickness(12, 9, 12, 9), Margin = isUser ? new Thickness(42, 0, 4, 8) : new Thickness(0, 0, 42, 8), CornerRadius = new CornerRadius(8), BorderThickness = new Thickness(1), BorderBrush = isUser ? UserCardBorder : AgentCardBorder, Background = isUser ? UserCardBackground : AgentCardBackground, Child = cardContent }; 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)); style.Setters.Add(new Setter(Control.PaddingProperty, new Thickness(12, 0, 12, 0))); style.Setters.Add(new Setter(Control.TemplateProperty, CreateSendButtonTemplate())); style.Triggers.Add(new Trigger { Property = UIElement.IsMouseOverProperty, Value = true, Setters = { new Setter(Control.BackgroundProperty, ButtonHoverBackground) } }); style.Triggers.Add(new Trigger { Property = ButtonBase.IsPressedProperty, Value = true, Setters = { new Setter(Control.BackgroundProperty, ButtonPressedBackground) } }); style.Triggers.Add(new Trigger { Property = UIElement.IsEnabledProperty, Value = false, Setters = { new Setter(Control.BackgroundProperty, DisabledButtonBackground), new Setter(Control.BorderBrushProperty, DisabledButtonBorder), new Setter(Control.ForegroundProperty, DisabledText), new Setter(UIElement.OpacityProperty, 1.0) } }); return style; } private static ControlTemplate CreateSendButtonTemplate() { var border = new FrameworkElementFactory(typeof(Border)); border.SetValue(Border.CornerRadiusProperty, new CornerRadius(7)); border.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty)); border.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Control.BorderBrushProperty)); border.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Control.BorderThicknessProperty)); var content = new FrameworkElementFactory(typeof(TextBlock)); content.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center); content.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center); content.SetValue(TextBlock.TextProperty, new TemplateBindingExtension(ContentControl.ContentProperty)); content.SetValue(TextBlock.ForegroundProperty, new TemplateBindingExtension(Control.ForegroundProperty)); content.SetValue(TextBlock.FontWeightProperty, new TemplateBindingExtension(Control.FontWeightProperty)); border.AppendChild(content); return new ControlTemplate(typeof(Button)) { VisualTree = border }; } } #endif