feat: we've got the configuration bits working

This commit is contained in:
Naomi Carrigan 2025-01-31 17:43:29 -08:00
commit fa1aaff5fe
Signed by: naomi
SSH Key Fingerprint: SHA256:rca1iUI2OhAM6n4FIUaFcZcicmri0jgocqKiTTAfrt8
20 changed files with 465 additions and 0 deletions

0
.gitignore vendored Normal file
View File

15
App.axaml Normal file
View File

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="app.App"
xmlns:local="using:app"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

47
App.axaml.cs Normal file
View File

@ -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<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
}

BIN
Assets/avalonia-logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

9
Models/ConfigModel.cs Normal file
View File

@ -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";
}

21
Program.cs Normal file
View File

@ -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<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

10
Styles.axaml Normal file
View File

@ -0,0 +1,10 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
</Styles>

31
ViewLocator.cs Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
namespace app.ViewModels;
using System;
using System.IO;
using System.Text.Json;
using app.Models;
public partial class AgentWindowViewModel : ViewModelBase
{
}

View File

@ -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
{
}

View File

@ -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<string> 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<string> 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<string> 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<ConfigModel>(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";
}
}

View File

@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace app.ViewModels;
public class ViewModelBase : ObservableObject
{
}

18
Views/AgentWindow.axaml Normal file
View File

@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:app.ViewModels"
x:DataType="vm:MainWindowViewModel"
x:Class="app.Views.AgentWindow">
<StackPanel>
<TextBlock Text="Deepgram Voice Agent" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid ShowGridLines="False" Margin="5" ColumnDefinitions="Auto, Auto" RowDefinitions="Auto, Auto, Auto, Auto">
<ToggleButton Grid.Row="0" Grid.Column="0" Command="{Binding ToggleConversation}">
<Panel>
<TextBlock Classes="start" Text="Start Listening"/>
<TextBlock Classes="stop" Text="Stop Listening"/>
</Panel>
</ToggleButton>
<Button Grid.Row="1" Grid.Column="0" Command="{Binding OpenConfig}">Edit Configuration</Button>
</Grid>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia.Controls;
using app.Models;
using app.ViewModels;
namespace app.Views;
public partial class AgentWindow : UserControl
{
public AgentWindow()
{
InitializeComponent();
}
}

23
Views/ConfigWindow.axaml Normal file
View File

@ -0,0 +1,23 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:app.ViewModels"
x:DataType="vm:MainWindowViewModel"
x:Class="app.Views.ConfigWindow">
<StackPanel>
<TextBlock Text="Configure your application!" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid ShowGridLines="False" Margin="5" ColumnDefinitions="Auto, Auto" RowDefinitions="Auto, Auto, Auto, Auto">
<Label Grid.Row="0" Grid.Column="0">API Key:</Label>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ApiKey, Mode=TwoWay}"/>
<Label Grid.Row="1" Grid.Column="0">Speak Model</Label>
<ComboBox Grid.Row="1" Grid.Column="1" SelectedItem="{Binding SpeakModel, Mode=TwoWay}" ItemsSource="{Binding SpeakModels}">
</ComboBox>
<Label Grid.Row="2" Grid.Column="0">Listen Model:</Label>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="0" SelectedItem="{Binding ListenModel, Mode=TwoWay}" ItemsSource="{Binding ListenModels}">
</ComboBox>
<Label Grid.Row="3" Grid.Column="0">Think Model:</Label>
<ComboBox Grid.Row="3" Grid.Column="1" SelectedIndex="0" SelectedItem="{Binding ThinkModel, Mode=TwoWay}" ItemsSource="{Binding ThinkModels}">
</ComboBox>
</Grid>
<Button Command="{Binding SaveConfig}">Save</Button>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
namespace app.Views;
public partial class ConfigWindow : UserControl
{
public ConfigWindow()
{
InitializeComponent();
}
}

34
Views/MainWindow.axaml Normal file
View File

@ -0,0 +1,34 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:app.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="app.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="Deepgram Voice Assistant">
<Window.Styles>
<Style Selector="ToggleButton .start">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="ToggleButton:checked .start">
<Setter Property="IsVisible" Value="True"/>
</Style>
<Style Selector="ToggleButton .stop">
<Setter Property="IsVisible" Value="True"/>
</Style>
<Style Selector="ToggleButton:checked .stop">
<Setter Property="IsVisible" Value="False"/>
</Style>
</Window.Styles>
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<ContentControl Content="{Binding CurrentView}"/>
</Window>

12
Views/MainWindow.axaml.cs Normal file
View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace app.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

28
app.csproj Normal file
View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
</ItemGroup>
</Project>

18
app.manifest Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="app.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>