commit fa1aaff5fe08ba4c5ddc9d0aaa603104bdb84634 Author: Naomi Carrigan Date: Fri Jan 31 17:43:29 2025 -0800 feat: we've got the configuration bits working diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..c9f3043 --- /dev/null +++ b/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..4fdc7c0 --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,47 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using System.Linq; +using Avalonia.Markup.Xaml; +using app.ViewModels; +using app.Views; + +namespace app; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } + + private static void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + var dataValidationPluginsToRemove = + BindingPlugins.DataValidators.OfType().ToArray(); + + // remove each entry found + foreach (var plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); + } + } +} \ No newline at end of file diff --git a/Assets/avalonia-logo.ico b/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/Assets/avalonia-logo.ico differ diff --git a/Models/ConfigModel.cs b/Models/ConfigModel.cs new file mode 100644 index 0000000..752e501 --- /dev/null +++ b/Models/ConfigModel.cs @@ -0,0 +1,9 @@ +namespace app.Models; + +public class ConfigModel +{ + public string ApiKey { get; set; } = ""; + public string SpeakModel { get; set; } = "aura-athena-en"; + public string ListenModel { get; set; } = "nova-2"; + public string ThinkModel { get; set; } = "claude-3-haiku-20240307"; +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..e830412 --- /dev/null +++ b/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace app; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/Styles.axaml b/Styles.axaml new file mode 100644 index 0000000..b525d2b --- /dev/null +++ b/Styles.axaml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/ViewLocator.cs b/ViewLocator.cs new file mode 100644 index 0000000..1dc4253 --- /dev/null +++ b/ViewLocator.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using app.ViewModels; + +namespace app; + +public class ViewLocator : IDataTemplate +{ + + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/ViewModels/AgentWindowViewModel.cs b/ViewModels/AgentWindowViewModel.cs new file mode 100644 index 0000000..4993b22 --- /dev/null +++ b/ViewModels/AgentWindowViewModel.cs @@ -0,0 +1,11 @@ +namespace app.ViewModels; + +using System; +using System.IO; +using System.Text.Json; +using app.Models; + +public partial class AgentWindowViewModel : ViewModelBase +{ + +} diff --git a/ViewModels/ConfigWindowViewModel.cs b/ViewModels/ConfigWindowViewModel.cs new file mode 100644 index 0000000..a8efcd9 --- /dev/null +++ b/ViewModels/ConfigWindowViewModel.cs @@ -0,0 +1,13 @@ +namespace app.ViewModels; + +using Avalonia.Interactivity; +using System; +using System.IO; +using System.Text.Json; +using app.Models; + +public partial class ConfigWindowViewModel : ViewModelBase +{ + + +} diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..c3a6cf5 --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,141 @@ +namespace app.ViewModels; + +using System; +using System.IO; +using System.Text.Json; +using System.Collections.Generic; +using Avalonia.Controls; +using app.Models; +using app.Views; + +public partial class MainWindowViewModel : ViewModelBase +{ + private UserControl _currentView; + public UserControl CurrentView + { + get => _currentView; + set => SetProperty(ref _currentView, value); + } + private ConfigModel config { get; set; } + public string ActionButtonText { get; set; } = "Start Conversation"; + public string ApiKey { get; set; } + public string SpeakModel { get; set; } + public string ListenModel { get; set; } + public string ThinkModel { get; set; } + public List SpeakModels { get; } = new() + { + "aura-asteria-en", "aura-luna-en", "aura-athena-en", "aura-stella-en", + "aura-hera-en", "aura-orion-en", "aura-arcas-en", "aura-perseus-en", + "aura-angus-en", "aura-orpheus-en", "aura-helios-en", "aura-zeus-en" + }; + + public List ListenModels { get; } = new() + { + "nova-2", "nova-2-meeting", "nova-2-phonecall", "nova-2-finance", + "nova-2-conversationalai", "nova-2-finance", "nova-2-voicemail", + "nova-2-video", "nova-2-medical", "nova-2-drivethru", "nova-2-automotive", + "nova-2-atc", "nova", "nova-phonecall", "nova-medical", "enhanced", + "enhanced-phonecall", "enhanced-meeting", "enhanced-finance", "base", + "base-phonecall", "base-meeting", "base-finance", "base-conversationalai", + "base-voicemail", "base-video", "whisper-tiny", "whisper-small", + "whisper-medium", "whisper-large", "whisper-base" + }; + + public List ThinkModels { get; } = new() + { + "gpt-4o-mini", "claude-3-haiku-20240307" + }; + + public MainWindowViewModel() + { + config = GetConfig(); + ApiKey = config.ApiKey; + SpeakModel = config.SpeakModel; + ListenModel = config.ListenModel; + ThinkModel = config.ThinkModel; + if (string.IsNullOrEmpty(config.ApiKey)) + { + CurrentView = new ConfigWindow(); + return; + } + CurrentView = new AgentWindow(); + } + private static string ConfigPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.config/deepgram-voice-assistant"; + private static string ConfigFilePath = ConfigPath + "/config.json"; + private static ConfigModel GetConfig() + { + // if config directory does not exist, create it + if (!Directory.Exists(ConfigPath)) + { + Console.WriteLine("Creating config directory"); + Directory.CreateDirectory(ConfigPath); + } + // check if file exists + if (!File.Exists(ConfigFilePath)) + { + // create file too + Console.WriteLine("Creating config file"); + using FileStream fs = File.Create(ConfigFilePath); + fs.Close(); + // write default config + Console.WriteLine("Writing default config"); + using StreamWriter writer = new(ConfigFilePath); + writer.WriteLine("{\n\t\"apiKey\": \"\",\n\t\"speakModel\": \"aura-athena-en\",\n\t\"listenModel\": \"nova-2\",\n\t\"thinkModel\": \"claude-3-haiku-20240307\"\n}"); + writer.Close(); + } + // Read the config file + using StreamReader reader = new(ConfigFilePath); + string json = reader.ReadToEnd(); + reader.Close(); + return JsonSerializer.Deserialize(json); + } + private static void UpdateConfig(ConfigModel config) + { + // Write the config file + using StreamWriter writer = new(ConfigFilePath); + writer.WriteLine(JsonSerializer.Serialize(config)); + writer.Close(); + } + + public void SaveConfig() + { + string speakModel = SpeakModel; + string listenModel = ListenModel; + string thinkModel = ThinkModel; + Console.WriteLine("Saving config"); + Console.WriteLine("ApiKey: " + ApiKey); + Console.WriteLine("SpeakModel: " + speakModel); + Console.WriteLine("ListenModel: " + listenModel); + Console.WriteLine("ThinkModel: " + thinkModel); + config = new() + { + ApiKey = ApiKey, + SpeakModel = speakModel, + ListenModel = listenModel, + ThinkModel = thinkModel + }; + UpdateConfig(config); + CurrentView = new AgentWindow(); + } + + public void OpenConfig() + { + CurrentView = new ConfigWindow(); + } + + public void ToggleConversation() { + if (ActionButtonText == "Start Conversation") { + StartConversation(); + } else { + EndConversation(); + } + } + + public void StartConversation() { + ActionButtonText = "End Conversation"; + } + + public void EndConversation() { + ActionButtonText = "Start Conversation"; + } +} diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..28a692b --- /dev/null +++ b/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace app.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} diff --git a/Views/AgentWindow.axaml b/Views/AgentWindow.axaml new file mode 100644 index 0000000..849e0c5 --- /dev/null +++ b/Views/AgentWindow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Views/AgentWindow.axaml.cs b/Views/AgentWindow.axaml.cs new file mode 100644 index 0000000..3dfb227 --- /dev/null +++ b/Views/AgentWindow.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using app.Models; +using app.ViewModels; + +namespace app.Views; + +public partial class AgentWindow : UserControl +{ + public AgentWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Views/ConfigWindow.axaml b/Views/ConfigWindow.axaml new file mode 100644 index 0000000..205ff26 --- /dev/null +++ b/Views/ConfigWindow.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Views/ConfigWindow.axaml.cs b/Views/ConfigWindow.axaml.cs new file mode 100644 index 0000000..29a30a4 --- /dev/null +++ b/Views/ConfigWindow.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using System; + +namespace app.Views; + +public partial class ConfigWindow : UserControl +{ + + public ConfigWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml new file mode 100644 index 0000000..9aa6592 --- /dev/null +++ b/Views/MainWindow.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..ed52a88 --- /dev/null +++ b/Views/MainWindow.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace app.Views; + +public partial class MainWindow : Window +{ + + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/app.csproj b/app.csproj new file mode 100644 index 0000000..936eabc --- /dev/null +++ b/app.csproj @@ -0,0 +1,28 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + + None + All + + + + diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..c1b6c8f --- /dev/null +++ b/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + +