#if WINDOWS using Aprillz.MewUI; using Aprillz.MewUI.Controls; namespace MeetingAssistant.Workflow; public sealed class MewUiWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService { private readonly IServiceProvider services; private readonly ILogger logger; private readonly object sync = new(); private bool isRunning; public MewUiWorkflowRulesEditorWindowService( IServiceProvider services, ILogger logger) { this.services = services; this.logger = logger; } public void Show() { lock (sync) { if (isRunning) { logger.LogInformation("Workflow rules editor UI is already running"); return; } isRunning = true; } logger.LogInformation("Starting workflow rules editor UI thread"); var thread = new Thread(RunWindow) { IsBackground = true, Name = "Meeting Assistant Rules Editor" }; thread.SetApartmentState(ApartmentState.STA); thread.Start(); } private void RunWindow() { try { logger.LogInformation("Workflow rules editor UI thread started"); var viewModel = services.GetRequiredService(); var window = new WorkflowRulesEditorMewWindow(viewModel); window.Run(); logger.LogInformation("Workflow rules editor UI window closed"); } catch (Exception exception) { logger.LogError(exception, "Workflow rules editor UI failed"); } finally { lock (sync) { isRunning = false; } } } } internal sealed class WorkflowRulesEditorMewWindow { private readonly WorkflowRulesEditorChatViewModel viewModel; private readonly StackPanel conversationPanel = new(); private readonly ScrollViewer conversationScroll = new(); private readonly MultiLineTextBox input = new(); private readonly Button sendButton = new(); private readonly EventHandler changedHandler; public WorkflowRulesEditorMewWindow(WorkflowRulesEditorChatViewModel viewModel) { this.viewModel = viewModel; changedHandler = (_, _) => RequestRender(); this.viewModel.Changed += changedHandler; } public void Run() { ConfigureTheme(); conversationPanel .Vertical() .Spacing(8); input .Height(92) .Wrap(true) .Placeholder("Ask to make a rule or to list identities") .OnTextChanged(text => viewModel.Draft = text) .OnKeyDown(args => { if (args.Key == Key.Enter && !args.ShiftKey) { args.Handled = true; _ = SendAsync(); } }); sendButton .Content("Send") .Width(84) .OnClick(() => _ = SendAsync()); var window = new Window() .Title("Edit rules and identities") .Resizable(680, 760, 520, 460) .Padding(0) .OnLoaded(Render) .OnClosed(() => viewModel.Changed -= changedHandler) .Content( new DockPanel() .LastChildFill() .Padding(12) .Spacing(10) .Children( new DockPanel() .LastChildFill() .Spacing(8) .DockBottom() .Children( sendButton.DockRight(), input), conversationScroll .AutoVerticalScroll() .NoHorizontalScroll() .Content(conversationPanel))); var icon = LoadWindowIcon(); if (icon is not null) { window.Icon = icon; } Application.Create() .UseTheme(ThemeVariant.Dark) .UseWin32() .UseDirect2D() .Run(window); } private static IconSource? LoadWindowIcon() { var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "meeting-assistant.ico"); return File.Exists(iconPath) ? IconSource.FromFile(iconPath) : null; } private static void ConfigureTheme() { ThemeManager.DefaultLightSeed = ThemeSeed.DefaultLight with { ButtonFace = Color.FromRgb(245, 245, 245) }; ThemeManager.DefaultDarkSeed = ThemeSeed.DefaultDark with { ButtonFace = Color.FromRgb(42, 46, 54) }; } private async Task SendAsync() { var sendTask = viewModel.SendAsync(); input.Text = viewModel.Draft; await sendTask; } private void RequestRender() { var dispatcher = Application.Current?.Dispatcher; if (dispatcher is null) { return; } dispatcher.BeginInvoke(Render); } private void Render() { var shouldAutoScroll = WorkflowRulesEditorScrollPolicy.ShouldAutoScroll( conversationScroll.VerticalOffset, conversationScroll.ViewportHeight, conversationPanel.ActualHeight); conversationPanel.Clear(); foreach (var message in viewModel.Messages) { conversationPanel.Add(CreateMessageCard(message)); } if (viewModel.IsThinking) { conversationPanel.Add(new TextBlock() .Text("Thinking...") .Foreground(Color.FromRgb(156, 166, 181)) .Margin(4, 2, 4, 2)); } sendButton.IsEnabled = !viewModel.IsThinking; if (shouldAutoScroll) { QueueScrollConversationToBottom(); } } private void QueueScrollConversationToBottom() { var dispatcher = Application.Current?.Dispatcher; if (dispatcher is null) { return; } dispatcher.BeginInvoke(DispatcherPriority.Idle, () => { ScrollConversationToBottom(); dispatcher.BeginInvoke(DispatcherPriority.Idle, ScrollConversationToBottom); }); } private void ScrollConversationToBottom() { conversationScroll.SetScrollOffsets( conversationScroll.HorizontalOffset, WorkflowRulesEditorScrollPolicy.GetBottomOffset( conversationScroll.ViewportHeight, conversationPanel.ActualHeight)); } private static Element CreateMessageCard(WorkflowRulesEditorChatMessage message) { var isUser = message.Role == WorkflowRulesEditorChatRole.User; return new Border() .Padding(10) .Margin(isUser ? new Thickness(42, 0, 4, 0) : new Thickness(0, 0, 42, 0)) .CornerRadius(8) .BorderThickness(1) .BorderBrush(isUser ? Color.FromRgb(68, 95, 132) : Color.FromRgb(69, 75, 86)) .Background(isUser ? Color.FromRgb(26, 48, 78) : Color.FromRgb(35, 39, 46)) .Child(WorkflowRulesEditorMarkdownRenderer.CreateContent(message.Content)); } } #endif