From 7ffdfea18fe124a8ba6d5c8dfe638a440609f23f Mon Sep 17 00:00:00 2001 From: tux Date: Sat, 5 Jul 2025 06:18:00 +0530 Subject: [PATCH] refactor: cleaup and implemented mvvm --- highminded.csproj | 3 +- models/ChatViewModel.cs | 9 ++ models/MainViewModel.cs | 13 +++ models/SettingsViewModel.cs | 12 +++ ui/controls/ChatUserControl.axaml | 12 +-- ui/controls/ChatUserControl.axaml.cs | 85 +++++++++--------- ui/controls/SettingsUserControl.axaml | 43 +++++----- ui/controls/SettingsUserControl.axaml.cs | 13 +-- ui/windows/MainWindow.axaml | 52 +++++++++--- ui/windows/MainWindow.axaml.cs | 104 ++++++++++++----------- utils/AudioCapture.cs | 52 +++--------- utils/InMemoryDB.cs | 41 +++++---- utils/SetttingsManager.cs | 20 ++--- 13 files changed, 249 insertions(+), 210 deletions(-) create mode 100644 models/ChatViewModel.cs create mode 100644 models/MainViewModel.cs create mode 100644 models/SettingsViewModel.cs diff --git a/highminded.csproj b/highminded.csproj index 241e9f7..701e24e 100644 --- a/highminded.csproj +++ b/highminded.csproj @@ -27,14 +27,15 @@ None All + - + diff --git a/models/ChatViewModel.cs b/models/ChatViewModel.cs new file mode 100644 index 0000000..47cd1a5 --- /dev/null +++ b/models/ChatViewModel.cs @@ -0,0 +1,9 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace highminded.models; + +public partial class ChatViewModel : ObservableObject +{ + [ObservableProperty] private string _content = ""; + [ObservableProperty] private string _prompt = ""; +} \ No newline at end of file diff --git a/models/MainViewModel.cs b/models/MainViewModel.cs new file mode 100644 index 0000000..9c9cf9d --- /dev/null +++ b/models/MainViewModel.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace highminded.models; + +public partial class MainViewModel : ObservableObject +{ + [ObservableProperty] private string _appName = "High Minded"; + [ObservableProperty] private string _hideShortcutKey = "SHIFT + \\"; + [ObservableProperty] private string _screenshotShortcutKey = "SHIFT + S"; + [ObservableProperty] private string _audioShortcutKey = "SHIFT + A"; + [ObservableProperty] private bool _isRecording = false; + [ObservableProperty] private bool _isHidden = true; +} \ No newline at end of file diff --git a/models/SettingsViewModel.cs b/models/SettingsViewModel.cs new file mode 100644 index 0000000..ea1cffd --- /dev/null +++ b/models/SettingsViewModel.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace highminded.models; + +public partial class SettingsViewModel : ObservableObject +{ + [ObservableProperty] private string _model = ""; + [ObservableProperty] private string _apiUrl = ""; + [ObservableProperty] private string _apiKey = ""; + [ObservableProperty] private string _screenshotPrompt = ""; + [ObservableProperty] private string _audioPrompt = ""; +} \ No newline at end of file diff --git a/ui/controls/ChatUserControl.axaml b/ui/controls/ChatUserControl.axaml index dc11dc7..735954e 100644 --- a/ui/controls/ChatUserControl.axaml +++ b/ui/controls/ChatUserControl.axaml @@ -2,14 +2,16 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="using:highminded.models" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="highminded.ui.controls.ChatUserControl"> + x:Class="highminded.ui.controls.ChatUserControl" + x:DataType="vm:ChatViewModel"> - - + + \ No newline at end of file diff --git a/ui/controls/ChatUserControl.axaml.cs b/ui/controls/ChatUserControl.axaml.cs index 6fdadfc..ac99f1a 100644 --- a/ui/controls/ChatUserControl.axaml.cs +++ b/ui/controls/ChatUserControl.axaml.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Avalonia.Input; +using highminded.models; using highminded.utils; using OpenAI.Chat; using Markdig; @@ -16,14 +17,18 @@ namespace highminded.ui.controls; public partial class ChatUserControl : UserControl { - private readonly MarkdownPipeline _pipeline = null!; - private AudioCapture _audioCapture = null!; + private readonly ChatViewModel _model; + private readonly MarkdownPipeline _pipeline; + private readonly AudioCapture _audioCapture; public ChatUserControl() { InitializeComponent(); - _pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseColorCode().Build(); + DataContext = InMemoryDb.Obj.ChatViewModel; + _audioCapture = new AudioCapture(); + _model = InMemoryDb.Obj.ChatViewModel; + _pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseColorCode().Build(); } public void StartRecord() @@ -33,42 +38,15 @@ public partial class ChatUserControl : UserControl var dirPath = Path.Combine(Environment.CurrentDirectory, "audio"); var filePath = Path.Combine(dirPath, fileName); Directory.CreateDirectory(dirPath); + InMemoryDb.Obj.MainViewModel.IsRecording = true; _audioCapture.StartRecording(filePath); } public void StopRecord() { - if (_audioCapture != null) - { - _audioCapture.StopRecording(); - SendAudio(); - } - } - - public async void SendAudio() - { - try - { - var dirPath = Path.Combine(Environment.CurrentDirectory, "audio"); - var latestAudio = new DirectoryInfo(dirPath).GetFiles().OrderByDescending(f => f.LastWriteTime).First(); - var filePath = Path.Combine(dirPath, latestAudio.Name); - var audioFileBytes = await File.ReadAllBytesAsync(filePath); - var audioBytes = BinaryData.FromBytes(audioFileBytes); - - List messages = - [ - new UserChatMessage( - ChatMessageContentPart.CreateTextPart(InMemoryDb.Obj.SettingsManager.Settings.AudioPrompt), - ChatMessageContentPart.CreateInputAudioPart(audioBytes, ChatInputAudioFormat.Wav) - ) - ]; - - await ProcessChatStreamAsync(messages); - } - catch (Exception err) - { - ResultBlock.Text = err.Message; - } + _audioCapture.StopRecording(); + InMemoryDb.Obj.MainViewModel.IsRecording = false; + SendAudio(); } public async void SendScreenshot() @@ -99,7 +77,33 @@ public partial class ChatUserControl : UserControl } catch (Exception err) { - ResultBlock.Text = err.Message; + _model.Content = err.Message; + } + } + + private async void SendAudio() + { + try + { + var dirPath = Path.Combine(Environment.CurrentDirectory, "audio"); + var latestAudio = new DirectoryInfo(dirPath).GetFiles().OrderByDescending(f => f.LastWriteTime).First(); + var filePath = Path.Combine(dirPath, latestAudio.Name); + var audioFileBytes = await File.ReadAllBytesAsync(filePath); + var audioBytes = BinaryData.FromBytes(audioFileBytes); + + List messages = + [ + new UserChatMessage( + ChatMessageContentPart.CreateTextPart(InMemoryDb.Obj.SettingsManager.Settings.AudioPrompt), + ChatMessageContentPart.CreateInputAudioPart(audioBytes, ChatInputAudioFormat.Wav) + ) + ]; + + await ProcessChatStreamAsync(messages); + } + catch (Exception err) + { + _model.Content = err.Message; } } @@ -109,15 +113,14 @@ public partial class ChatUserControl : UserControl { if (e.Key != Key.Enter) return; - var prompt = PromptBox.Text; - if (prompt is null) return; - PromptBox.Clear(); - + var prompt = _model.Prompt; + if (prompt == string.Empty) return; + _model.Prompt = string.Empty; await ProcessChatStreamAsync(prompt); } catch (Exception err) { - ResultBlock.Text = err.Message; + _model.Content = err.Message; } } @@ -140,7 +143,7 @@ public partial class ChatUserControl : UserControl responseBuilder.Append(token); var html = Markdig.Markdown.ToHtml(responseBuilder.ToString(), _pipeline); - ResultBlock.Text = html; + _model.Content = html; } } } \ No newline at end of file diff --git a/ui/controls/SettingsUserControl.axaml b/ui/controls/SettingsUserControl.axaml index adb69d9..5997fd9 100644 --- a/ui/controls/SettingsUserControl.axaml +++ b/ui/controls/SettingsUserControl.axaml @@ -2,39 +2,42 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="using:highminded.models" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="highminded.ui.controls.SettingsUserControl"> + x:Class="highminded.ui.controls.SettingsUserControl" + x:DataType="vm:SettingsViewModel"> + + - - - + - + - + - + - + - - - - + + + + + + + + + - - - - - - diff --git a/ui/windows/MainWindow.axaml.cs b/ui/windows/MainWindow.axaml.cs index 6fd2123..0e97fcb 100644 --- a/ui/windows/MainWindow.axaml.cs +++ b/ui/windows/MainWindow.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; using highminded.ui.controls; +using highminded.utils; using SharpHook; using SharpHook.Data; @@ -36,13 +37,14 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); + DataContext = InMemoryDb.Obj.MainViewModel; + UControl.Content = _chatUserControl; ChatBtnActive(); HideOverlay(); // Global Hotkey _hook.KeyPressed += OnKeyPressed; _hook.RunAsync(); - _chatUserControl.SendAudio(); } private void OnPointerPressed(object? sender, PointerPressedEventArgs e) @@ -74,56 +76,6 @@ public partial class MainWindow : Window SettingsBtn.Background = new SolidColorBrush(Color.FromArgb(25, 255, 255, 255)); } - private void OnKeyPressed(object? sender, KeyboardHookEventArgs e) - { - bool hasCtrl = (e.RawEvent.Mask & EventMask.Ctrl) != EventMask.None; - bool hasAlt = (e.RawEvent.Mask & EventMask.Alt) != EventMask.None; - bool hasShift = (e.RawEvent.Mask & EventMask.Shift) != EventMask.None; - bool hasH = e.Data.KeyCode == KeyCode.VcH; - bool hasBackslash = e.Data.KeyCode == KeyCode.VcBackslash; - bool hasA = e.Data.KeyCode == KeyCode.VcA; - bool hasS = e.Data.KeyCode == KeyCode.VcS; - bool hasQ = e.Data.KeyCode == KeyCode.VcQ; - - if (hasCtrl && hasShift && hasAlt && hasH) - { - ShowOverlay(); - } - - if (hasShift && hasS) - { - Dispatcher.UIThread.Post(() => { _chatUserControl.SendScreenshot(); }); - } - - if (hasShift && hasA) - { - Dispatcher.UIThread.Post(() => { _chatUserControl.StartRecord(); }); - } - - if (hasAlt && hasShift && hasQ) - { - Dispatcher.UIThread.Post(() => { Environment.Exit(0); }); - } - - if (hasShift && hasBackslash) - { - Dispatcher.UIThread.Post(() => - { - if (WindowState == WindowState.Minimized || !IsVisible) - { - Show(); - WindowState = WindowState.Normal; - Activate(); - Topmost = true; - } - else - { - Hide(); - } - }); - } - } - private void ShowOverlay() { var handle = TryGetPlatformHandle(); @@ -188,4 +140,54 @@ public partial class MainWindow : Window { Hide(); } + + private void OnKeyPressed(object? sender, KeyboardHookEventArgs e) + { + bool hasCtrl = (e.RawEvent.Mask & EventMask.Ctrl) != EventMask.None; + bool hasAlt = (e.RawEvent.Mask & EventMask.Alt) != EventMask.None; + bool hasShift = (e.RawEvent.Mask & EventMask.Shift) != EventMask.None; + bool hasH = e.Data.KeyCode == KeyCode.VcH; + bool hasBackslash = e.Data.KeyCode == KeyCode.VcBackslash; + bool hasA = e.Data.KeyCode == KeyCode.VcA; + bool hasS = e.Data.KeyCode == KeyCode.VcS; + bool hasQ = e.Data.KeyCode == KeyCode.VcQ; + + if (hasCtrl && hasShift && hasAlt && hasH) + { + ShowOverlay(); + } + + if (hasShift && hasS) + { + Dispatcher.UIThread.Post(() => { _chatUserControl.SendScreenshot(); }); + } + + if (hasShift && hasA) + { + Dispatcher.UIThread.Post(() => { _chatUserControl.StartRecord(); }); + } + + if (hasAlt && hasShift && hasQ) + { + Dispatcher.UIThread.Post(() => { Environment.Exit(0); }); + } + + if (hasShift && hasBackslash) + { + Dispatcher.UIThread.Post(() => + { + if (WindowState == WindowState.Minimized || !IsVisible) + { + Show(); + WindowState = WindowState.Normal; + Activate(); + Topmost = true; + } + else + { + Hide(); + } + }); + } + } } \ No newline at end of file diff --git a/utils/AudioCapture.cs b/utils/AudioCapture.cs index 7eaf5b1..7a3f524 100644 --- a/utils/AudioCapture.cs +++ b/utils/AudioCapture.cs @@ -1,51 +1,27 @@ -using NAudio.CoreAudioApi; -using NAudio.Wave; +using System.IO; +using SoundFlow.Backends.MiniAudio; +using SoundFlow.Components; +using SoundFlow.Enums; namespace highminded.utils; public class AudioCapture { - private WasapiLoopbackCapture capture; - private WaveFileWriter writer; - private string outputFilePath; + private readonly MiniAudioEngine _audioEngine = new(48000, Capability.Loopback); + private Stream? _fileStream; + private Recorder? _recorder; - public AudioCapture() + public void StartRecording(string filePath) { - capture = new WasapiLoopbackCapture(); - } - - public void StartRecording(string outPath) - { - writer = new WaveFileWriter(outPath, capture.WaveFormat); - capture.DataAvailable += Capture_DataAvailable; - capture.RecordingStopped += Capture_RecordingStopped; - capture.StartRecording(); + _fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None); + _recorder = new Recorder(_fileStream, sampleRate: 48000); + _recorder.StartRecording(); } public void StopRecording() { - capture.StopRecording(); - } - - public bool IsRecording() - { - if (capture.CaptureState == CaptureState.Capturing || capture.CaptureState == CaptureState.Starting) - { - return true; - } - - return false; - } - - private void Capture_DataAvailable(object sender, WaveInEventArgs e) - { - writer.Write(e.Buffer, 0, e.BytesRecorded); - } - - private void Capture_RecordingStopped(object sender, StoppedEventArgs e) - { - writer.Flush(); - writer.Dispose(); - capture.Dispose(); + _recorder?.StopRecording(); + _recorder?.Dispose(); + _fileStream?.Dispose(); } } \ No newline at end of file diff --git a/utils/InMemoryDB.cs b/utils/InMemoryDB.cs index 6964685..9f6a069 100644 --- a/utils/InMemoryDB.cs +++ b/utils/InMemoryDB.cs @@ -1,5 +1,6 @@ using System; using System.ClientModel; +using highminded.models; using OpenAI; using OpenAI.Chat; @@ -7,37 +8,45 @@ namespace highminded.utils; public class InMemoryDb { - internal ChatClient ChatClient; - internal SettingsManager SettingsManager; + public static readonly InMemoryDb Obj = new InMemoryDb(); + + internal readonly SettingsManager SettingsManager; + internal readonly MainViewModel MainViewModel; + internal readonly ChatViewModel ChatViewModel; + internal readonly SettingsViewModel SettingsViewModel; + internal ChatClient? ChatClient; // Initialize Singleton Class private InMemoryDb() { - SettingsManager = new SettingsManager(); - if (SettingsManager.Settings.ApiKey != null) + SettingsManager = new SettingsManager(); + + MainViewModel = new MainViewModel(); + ChatViewModel = new ChatViewModel(); + SettingsViewModel = new SettingsViewModel(); + SettingsViewModel = SettingsManager.Settings; + + if (SettingsManager.Settings.ApiKey != string.Empty) { - InitOpenAIClient(); + InitOpenAiClient(); } } - public void InitOpenAIClient() + public void SaveSettings() + { + SettingsManager.Save(); + InitOpenAiClient(); + } + + private void InitOpenAiClient() { ChatClient = new ChatClient( model: SettingsManager.Settings.Model, credential: new ApiKeyCredential(SettingsManager.Settings.ApiKey), options: new OpenAIClientOptions { - Endpoint = new Uri(SettingsManager.Settings.ApiURL) + Endpoint = new Uri(SettingsManager.Settings.ApiUrl) } ); } - - public void SaveSettings(AppSettings settings) - { - SettingsManager.Settings = settings; - SettingsManager.Save(); - InitOpenAIClient(); - } - - public static readonly InMemoryDb Obj = new InMemoryDb(); } \ No newline at end of file diff --git a/utils/SetttingsManager.cs b/utils/SetttingsManager.cs index 3179efb..5031634 100644 --- a/utils/SetttingsManager.cs +++ b/utils/SetttingsManager.cs @@ -4,27 +4,17 @@ using System.Text.Json; namespace highminded.utils; -public class AppSettings -{ - public string Model { get; set; } - public string ApiURL { get; set; } - public string ApiKey { get; set; } - public string ScreenshotPrompt { get; set; } - public string AudioPrompt { get; set; } - -} - public class SettingsManager where T : class, new() { private readonly string _settingsPath; public T Settings { get; internal set; } - public SettingsManager(string appName = "highminded") + public SettingsManager() { - _settingsPath = Path.Combine(Environment.CurrentDirectory, "settings.json"); - Settings = Load(); - } - + _settingsPath = Path.Combine(Environment.CurrentDirectory, "settings.json"); + Settings = Load(); + } + private T Load() { if (!File.Exists(_settingsPath))