Public Access
405 lines
14 KiB
C#
405 lines
14 KiB
C#
#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 = "Settings and logs";
|
|
|
|
private readonly IServiceProvider services;
|
|
private readonly WorkflowRulesEditorMarkdownLinkResolver linkResolver;
|
|
private readonly ILogger<WpfWorkflowRulesEditorWindowService> logger;
|
|
private readonly object sync = new();
|
|
private Dispatcher? dispatcher;
|
|
private WorkflowRulesEditorWpfWindow? window;
|
|
private bool isStarted;
|
|
|
|
public WpfWorkflowRulesEditorWindowService(
|
|
IServiceProvider services,
|
|
WorkflowRulesEditorMarkdownLinkResolver linkResolver,
|
|
ILogger<WpfWorkflowRulesEditorWindowService> 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<WorkflowRulesEditorChatViewModel>(),
|
|
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 message in viewModel.Messages)
|
|
{
|
|
conversationPanel.Children.Add(CreateMessageCard(message));
|
|
}
|
|
|
|
if (viewModel.IsThinking)
|
|
{
|
|
conversationPanel.Children.Add(new TextBlock
|
|
{
|
|
Text = viewModel.ActivityMessage,
|
|
Foreground = MutedText,
|
|
Margin = new Thickness(4, 2, 4, 2)
|
|
});
|
|
}
|
|
|
|
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 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
|