#if WINDOWS using System.Runtime.InteropServices; using System.Text; using Aprillz.MewUI; using Aprillz.MewUI.Controls; namespace MeetingAssistant.Workflow; public sealed class MewUiWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService { internal const string WindowTitle = "Settings and logs"; 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"); if (WorkflowRulesEditorWindowActivation.TryActivate(WindowTitle)) { return; } logger.LogWarning("Workflow rules editor UI was marked running, but no window could be activated; starting a new UI thread"); isRunning = false; } 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 me what I can do for you") .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(MewUiWorkflowRulesEditorWindowService.WindowTitle) .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; var copyButton = new Button() .Content("⧉") .Width(28) .Height(24) .ToolTip("Copy message") .OnClick(() => WorkflowRulesEditorClipboard.SetText(message.Content)); copyButton.IsVisible = false; var copyRow = new DockPanel() .LastChildFill(false) .Children(copyButton.DockRight()); var cardContent = new StackPanel() .Vertical() .Spacing(4) .Children( copyRow, WorkflowRulesEditorMarkdownRenderer.CreateContent(message.Content)); var card = 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)) .OnMouseEnter(() => copyButton.IsVisible = true) .OnMouseLeave(() => copyButton.IsVisible = false) .Child(cardContent); return card; } } internal static class WorkflowRulesEditorWindowActivation { private const int SwRestore = 9; public static bool TryActivate(string title) { if (string.IsNullOrWhiteSpace(title)) { return false; } var handle = FindWindow(null, title); if (handle == IntPtr.Zero) { return false; } ShowWindow(handle, SwRestore); return SetForegroundWindow(handle); } [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string? lpClassName, string lpWindowName); [DllImport("user32.dll", SetLastError = true)] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll", SetLastError = true)] private static extern bool SetForegroundWindow(IntPtr hWnd); } internal static class WorkflowRulesEditorClipboard { private const uint CfUnicodeText = 13; private const uint GmemMoveable = 0x0002; private const uint GmemZeroInit = 0x0040; public static void SetText(string text) { var bytes = Encoding.Unicode.GetBytes((text ?? "") + '\0'); var handle = GlobalAlloc(GmemMoveable | GmemZeroInit, (UIntPtr)bytes.Length); if (handle == IntPtr.Zero) { return; } var locked = GlobalLock(handle); if (locked == IntPtr.Zero) { GlobalFree(handle); return; } Marshal.Copy(bytes, 0, locked, bytes.Length); GlobalUnlock(handle); if (!OpenClipboard(IntPtr.Zero)) { GlobalFree(handle); return; } try { EmptyClipboard(); if (SetClipboardData(CfUnicodeText, handle) != IntPtr.Zero) { handle = IntPtr.Zero; } } finally { CloseClipboard(); if (handle != IntPtr.Zero) { GlobalFree(handle); } } } [DllImport("user32.dll", SetLastError = true)] private static extern bool OpenClipboard(IntPtr hWndNewOwner); [DllImport("user32.dll", SetLastError = true)] private static extern bool EmptyClipboard(); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem); [DllImport("user32.dll", SetLastError = true)] private static extern bool CloseClipboard(); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GlobalLock(IntPtr hMem); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GlobalUnlock(IntPtr hMem); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GlobalFree(IntPtr hMem); } #endif