feat: start rewriting as tauri app

This commit is contained in:
Naomi Carrigan 2025-02-04 16:46:06 -08:00
parent aeebbc7f0a
commit 20ff35d4ce
Signed by: naomi
SSH Key Fingerprint: SHA256:rca1iUI2OhAM6n4FIUaFcZcicmri0jgocqKiTTAfrt8
78 changed files with 17707 additions and 496 deletions

44
.gitignore vendored
View File

@ -1,2 +1,42 @@
/bin
/obj
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@ -1,15 +0,0 @@
<Application
x:Class="app.App"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:app"
RequestedThemeVariant="Dark">
<Application.DataTemplates>
<local:ViewLocator />
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -1,47 +0,0 @@
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);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@ -1,9 +0,0 @@
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";
}

View File

@ -1,21 +0,0 @@
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();
}

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Tauri + Angular
This template should help get you started developing with Tauri and Angular.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template).

View File

@ -1,31 +0,0 @@
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

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

View File

@ -1,13 +0,0 @@
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

@ -1,7 +0,0 @@
namespace app.ViewModels;
public partial class HomeWindowViewModel : ViewModelBase
{
}

View File

@ -1,144 +0,0 @@
namespace app.ViewModels;
using System;
using System.Threading.Tasks;
using System.IO;
using System.Text.Json;
using System.Collections.Generic;
using System.Net.WebSockets;
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; }
private ClientWebSocket ws { 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()
{
ws = new();
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 HomeWindow();
}
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 HomeWindow();
}
public void OpenConfig()
{
CurrentView = new ConfigWindow();
}
public void CancelConfig() {
CurrentView = new HomeWindow();
}
public async Task StartAgent() {
// ws.Options.SetRequestHeader("Authorization", "token " + config.ApiKey);
// await ws.ConnectAsync(new Uri("wss://agent.deepgram.com/agent"), System.Threading.CancellationToken.None);
CurrentView = new AgentWindow();
}
public async Task EndConversation() {
CurrentView = new HomeWindow();
// await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Conversation ended", System.Threading.CancellationToken.None);
}
}

View File

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

View File

@ -1,12 +0,0 @@
<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 VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Deepgram Voice Agent" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid ShowGridLines="False" Margin="5" ColumnDefinitions="Auto, Auto" RowDefinitions="Auto, Auto, Auto, Auto">
<Button Grid.Row="0" Grid.Column="0" Background="Red" CornerRadius="10" Command="{Binding EndConversation}">End Conversation</Button>
</Grid>
</StackPanel>
</UserControl>

View File

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

View File

@ -1,26 +0,0 @@
<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 VerticalAlignment="Center" HorizontalAlignment="Center">
<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>
<Grid ShowGridLines="False" Margin="5" ColumnDefinitions="Auto, Auto" RowDefinitions="Auto">
<Button Grid.Row="0" Grid.Column="0" CornerRadius="10" Background="Blue" Command="{Binding SaveConfig}">Save</Button>
<Button Grid.Row="0" Grid.Column="1" CornerRadius="10" Foreground="Black" Background="Yellow" Command="{Binding CancelConfig}" IsVisible="{Binding ApiKey}">Cancel</Button>
</Grid>
</StackPanel>
</UserControl>

View File

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

View File

@ -1,13 +0,0 @@
<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.HomeWindow">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Deepgram Voice Agent" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid ShowGridLines="False" Margin="5" ColumnDefinitions="Auto, Auto" RowDefinitions="Auto">
<Button Grid.Row="0" Grid.Column="0" CornerRadius="10" Background="Green" Command="{Binding StartAgent}">Start Agent</Button>
<Button Grid.Row="0" Grid.Column="1" CornerRadius="10" Foreground="Black" Background="Yellow" Command="{Binding OpenConfig}">Edit Configuration</Button>
</Grid>
</StackPanel>
</UserControl>

View File

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

View File

@ -1,20 +0,0 @@
<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">
<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>

View File

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

67
angular.json Normal file
View File

@ -0,0 +1,67 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"cli": {
"analytics": false
},
"projects": {
"deepgram-voice-assistant": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/deepgram-voice-assistant",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/assets"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"port": 1420
},
"configurations": {
"production": {
"buildTarget": "deepgram-voice-assistant:build:production"
},
"development": {
"buildTarget": "deepgram-voice-assistant:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
}

View File

@ -1,28 +0,0 @@
<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>

View File

@ -1,18 +0,0 @@
<?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>

View File

@ -0,0 +1,7 @@
{
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"angular.ng-template"
]
}

5
eslint.config.js Normal file
View File

@ -0,0 +1,5 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig
];

View File

@ -1,24 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "app", "app.csproj", "{4195E981-2360-7259-32C6-BB2CA129A715}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4195E981-2360-7259-32C6-BB2CA129A715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4195E981-2360-7259-32C6-BB2CA129A715}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4195E981-2360-7259-32C6-BB2CA129A715}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4195E981-2360-7259-32C6-BB2CA129A715}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7CAB8223-BDA9-44EA-9FB2-675BF9E2D383}
EndGlobalSection
EndGlobal

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "deepgram-voice-assistant",
"version": "0.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"tauri": "tauri"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.1.4",
"@angular/common": "^19.1.4",
"@angular/compiler": "^19.1.4",
"@angular/core": "^19.1.4",
"@angular/forms": "^19.1.4",
"@angular/platform-browser": "^19.1.4",
"@angular/platform-browser-dynamic": "^19.1.4",
"@angular/router": "^19.1.4",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"rxjs": "~7.8.1",
"tslib": "^2.8.1",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.1.5",
"@angular/cli": "^19.1.5",
"@angular/compiler-cli": "^19.1.4",
"@nhcarrigan/eslint-config": "5.1.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@tauri-apps/cli": "^2",
"@types/jasmine": "~5.1.5",
"@types/node": "22.13.1",
"eslint": "9.19.0",
"jasmine-core": "~5.5.0",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.3"
}
}

11570
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

7
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5433
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "deepgram-voice-assistant"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "deepgram_voice_assistant_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

14
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,14 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
deepgram_voice_assistant_lib::run()
}

35
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,35 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "deepgram-voice-assistant",
"version": "0.1.0",
"identifier": "com.deepgram-voice-assistant.app",
"build": {
"beforeDevCommand": "pnpm start",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist/deepgram-voice-assistant/browser"
},
"app": {
"windows": [
{
"title": "deepgram-voice-assistant",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

View File

@ -0,0 +1 @@
<p>agent works!</p>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AgentComponent } from './agent.component';
describe('AgentComponent', () => {
let component: AgentComponent;
let fixture: ComponentFixture<AgentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AgentComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AgentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-agent',
standalone: true,
imports: [],
templateUrl: './agent.component.html',
styleUrl: './agent.component.css'
})
export class AgentComponent {
}

32
src/app/app.component.css Normal file
View File

@ -0,0 +1,32 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin-top: 0;
}
main {
text-align: center;
color: #ffffff;
background: #000000;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
overflow: hidden;
}

View File

@ -0,0 +1,3 @@
<main>
<router-outlet></router-outlet>
</main>

14
src/app/app.component.ts Normal file
View File

@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
}

8
src/app/app.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { ApplicationConfig } from "@angular/core";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};

10
src/app/app.routes.ts Normal file
View File

@ -0,0 +1,10 @@
import { Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { ConfigComponent } from "./config/config.component";
import { AgentComponent } from "./agent/agent.component";
export const routes: Routes = [
{ path: "", pathMatch: "full", component: HomeComponent },
{ path: "config", component: ConfigComponent },
{ path: "agent", component: AgentComponent }
];

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ConfigService } from '../../../../../.config/emacs/backups/!home!naomi!code!deepgram!deepgram-voice-assistant!src!app!config.service.ts~';
describe('ConfigService', () => {
let service: ConfigService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ConfigService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

19
src/app/config.service.ts Normal file
View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Config } from "./interfaces/Config";
@Injectable({
providedIn: 'root'
})
export class ConfigService {
public getConfig(): Config | null {
const config = localStorage.getItem("config");
if (!config) {
return null;
}
return JSON.parse(config) as Config;
}
public setConfig(config: Config) {
localStorage.setItem("config", JSON.stringify(config));
}
}

View File

@ -0,0 +1,22 @@
form {
display: grid;
grid-template-columns: 200px 400px;
margin: auto;
width: 600px;
}
input, select {
margin-bottom: 1rem;
}
.green {
background: green;
}
.red {
background: red;
}
button {
border: 2px solid white;
}

View File

@ -0,0 +1,61 @@
<h1>Configuration</h1>
<p>These settings determine how your Deepgram Voice Agent behaves.</p>
<form>
<label for="apiKey">API Key:</label>
<input name="apiKey" id="apiKey" type="text" [formControl]="apiKey">
<label for="speak">Speak Model:</label>
<select name="speak" id="speak" [formControl]="speakModel">
<option value="aura-asteria-en">aura-asteria-en</option>
<option value="aura-luna-en">aura-luna-en</option>
<option value="aura-athena-en">aura-athena-en</option>
<option value="aura-stella-en">aura-stella-en</option>
<option value="aura-hera-en">aura-hera-en</option>
<option value="aura-orion-en">aura-orion-en</option>
<option value="aura-arcas-en">aura-arcas-en</option>
<option value="aura-perseus-en">aura-perseus-en</option>
<option value="aura-angus-en">aura-angus-en</option>
<option value="aura-orpheus-en">aura-orpheus-en</option>
<option value="aura-helios-en">aura-helios-en</option>
<option value="aura-zeus-en">aura-zeus-en</option>
</select>
<label for="listen">Listen Model:</label>
<select name="listen" id="listen" [formControl]="listenModel">
<option value="nova-2">nova-2</option>
<option value="nova-2-meeting">nova-2-meeting</option>
<option value="nova-2-phonecall">nova-2-phonecall</option>
<option value="nova-2-finance">nova-2-finance</option>
<option value="nova-2-conversationalai">nova-2-conversationalai</option>
<option value="nova-2-voicemail">nova-2-voicemail</option>
<option value="nova-2-video">nova-2-video</option>
<option value="nova-2-medical">nova-2-medical</option>
<option value="nova-2-drivethru">nova-2-drivethru</option>
<option value="nova-2-automotive">nova-2-automotive</option>
<option value="nova-2-atc">nova-2-atc</option>
<option value="nova">nova</option>
<option value="nova-phonecall">nova-phonecall</option>
<option value="nova-medical">nova-medical</option>
<option value="enhanced">enhanced</option>
<option value="enhanced-phonecall">enhanced-phonecall</option>
<option value="enhanced-meeting">enhanced-meeting</option>
<option value="enhanced-finance">enhanced-finance</option>
<option value="base">base</option>
<option value="base-phonecall">base-phonecall</option>
<option value="base-meeting">base-meeting</option>
<option value="base-finance">base-finance</option>
<option value="base-conversationalai">base-conversationalai</option>
<option value="base-voicemail">base-voicemail</option>
<option value="base-video">base-video</option>
<option value="whisper-tiny">whisper-tiny</option>
<option value="whisper-small">whisper-small</option>
<option value="whisper-medium">whisper-medium</option>
<option value="whisper-large">whisper-large</option>
<option value="whisper-base">whisper-base</option>
</select>
<label for="think">Think Model:</label>
<select name="think" id="think" [formControl]="thinkModel">
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
</select>
</form>
<button class="green" (click)="save()">Save Configuration</button>
<button class="red" (click)="cancel()">Cancel</button>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfigComponent } from './config.component';
describe('ConfigComponent', () => {
let component: ConfigComponent;
let fixture: ComponentFixture<ConfigComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ConfigComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ConfigComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,66 @@
import { Component } from '@angular/core';
import { AbstractControl, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { ConfigService } from '../config.service';
import { Config } from '../interfaces/Config';
import type { SpeakModel, ThinkModel, ListenModel } from "../interfaces/Settings";
@Component({
selector: 'app-config',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './config.component.html',
styleUrl: './config.component.css'
})
export class ConfigComponent {
public apiKey = new FormControl("", [
Validators.required
]);
public speakModel = new FormControl("aura-athena-en", [
Validators.required,
() => (control: AbstractControl) => ["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"].includes(control.value)
]);
public listenModel = new FormControl("nova-2", [
Validators.required,
() => (control: AbstractControl) => [ "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"
].includes(control.value)
]);
public thinkModel = new FormControl("claude-3-haiku-20240307", [
Validators.required,
() => (control: AbstractControl) => ["claude-3-haiku-20240307", "gpt-4o-mini"].includes(control.value)
]);
public save() {
const config: Config = {
apiKey: this.apiKey.value ?? "",
speakModel: this.speakModel.value as SpeakModel,
listenModel: this.listenModel.value as ListenModel,
thinkModel: this.thinkModel.value as ThinkModel
}
this.configService.setConfig(config);
window.location.pathname = "/";
}
public cancel() {
window.location.pathname = "/";
}
constructor(private configService: ConfigService) {
const config = this.configService.getConfig();
if (!config) {
return;
}
this.apiKey.setValue(config.apiKey);
this.speakModel.setValue(config.speakModel);
this.listenModel.setValue(config.listenModel);
this.thinkModel.setValue(config.thinkModel);
}
}

View File

@ -0,0 +1,22 @@
.btn {
border: 2px solid black;
width: 200px;
text-decoration: none;
}
.green {
background: green;
color: white;
}
.yellow {
background: yellow;
color: black;
}
.grid {
display: grid;
width: 500px;
grid-template-columns: 250px 250px;
margin: auto;
}

View File

@ -0,0 +1,6 @@
<h1>Deepgram Voice Assistant</h1>
<p>Welcome to your new favourite voice-powered assistant.</p>
<div class="grid">
<a href="/agent" class="green btn">Start Agent</a>
<a href="/config" class="yellow btn">Configuration</a>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
standalone: true,
imports: [],
templateUrl: './home.component.html',
styleUrl: './home.component.css'
})
export class HomeComponent {
}

View File

@ -0,0 +1,8 @@
import { ListenModel, SpeakModel, ThinkModel } from "./Settings";
export interface Config {
apiKey: string;
speakModel: SpeakModel;
thinkModel: ThinkModel;
listenModel: ListenModel;
}

View File

@ -0,0 +1,5 @@
export type ListenModel = "nova-2" | "nova-2-meeting" | "nova-2-phonecall" | "nova-2-finance" | "nova-2-conversationalai" | "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";
export type ThinkModel = "gpt-4o-mini" | "claude-3-haiku-20240307";
export type SpeakModel = "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-oprheus-en" | "aura-helios-en" | "aura-zeus-en";

12
src/index.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Tauri + Angular</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body style="margin: 0">
<app-root></app-root>
</body>
</html>

7
src/main.ts Normal file
View File

@ -0,0 +1,7 @@
import { bootstrapApplication } from "@angular/platform-browser";
import { appConfig } from "./app/app.config";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err),
);

10
tsconfig.app.json Normal file
View File

@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"outDir": "./dist/out-tsc",
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}