Files
meeting-assistant/MeetingAssistant/Workflow/MewUiWorkflowRulesEditorWindowService.Windows.cs
T
codex 693f52afee
PR and Push Build/Test / build-and-test (push) Successful in 7m0s
Add tray rules and identities editor
2026-05-28 01:28:16 +02:00

252 lines
7.4 KiB
C#

#if WINDOWS
using Aprillz.MewUI;
using Aprillz.MewUI.Controls;
namespace MeetingAssistant.Workflow;
public sealed class MewUiWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService
{
private readonly IServiceProvider services;
private readonly ILogger<MewUiWorkflowRulesEditorWindowService> logger;
private readonly object sync = new();
private bool isRunning;
public MewUiWorkflowRulesEditorWindowService(
IServiceProvider services,
ILogger<MewUiWorkflowRulesEditorWindowService> 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<WorkflowRulesEditorChatViewModel>();
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