WIP push-to-talk Letta chat frontend

add persistent credential manager

graham.systems bbe0a520 fbe5fedb

verified
Changed files
+263 -14
src
routes
src-tauri
+102 -1
src-tauri/Cargo.lock
··· 874 874 checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 875 875 876 876 [[package]] 877 + name = "dbus" 878 + version = "0.9.9" 879 + source = "registry+https://github.com/rust-lang/crates.io-index" 880 + checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" 881 + dependencies = [ 882 + "libc", 883 + "libdbus-sys", 884 + "windows-sys 0.59.0", 885 + ] 886 + 887 + [[package]] 888 + name = "dbus-secret-service" 889 + version = "4.1.0" 890 + source = "registry+https://github.com/rust-lang/crates.io-index" 891 + checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" 892 + dependencies = [ 893 + "dbus", 894 + "zeroize", 895 + ] 896 + 897 + [[package]] 877 898 name = "deranged" 878 899 version = "0.4.0" 879 900 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2052 2073 ] 2053 2074 2054 2075 [[package]] 2076 + name = "keyring" 2077 + version = "3.6.3" 2078 + source = "registry+https://github.com/rust-lang/crates.io-index" 2079 + checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 2080 + dependencies = [ 2081 + "byteorder", 2082 + "dbus-secret-service", 2083 + "log", 2084 + "security-framework 2.11.1", 2085 + "security-framework 3.3.0", 2086 + "windows-sys 0.60.2", 2087 + "zeroize", 2088 + ] 2089 + 2090 + [[package]] 2055 2091 name = "kuchikiki" 2056 2092 version = "0.8.8-speedreader" 2057 2093 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2098 2134 version = "0.2.175" 2099 2135 source = "registry+https://github.com/rust-lang/crates.io-index" 2100 2136 checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 2137 + 2138 + [[package]] 2139 + name = "libdbus-sys" 2140 + version = "0.2.6" 2141 + source = "registry+https://github.com/rust-lang/crates.io-index" 2142 + checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" 2143 + dependencies = [ 2144 + "pkg-config", 2145 + ] 2101 2146 2102 2147 [[package]] 2103 2148 name = "libloading" ··· 2242 2287 "cpal", 2243 2288 "dasp", 2244 2289 "futures-util", 2290 + "keyring", 2245 2291 "serde", 2246 2292 "serde_json", 2293 + "strum", 2247 2294 "tauri", 2248 2295 "tauri-build", 2249 2296 "tauri-plugin-opener", ··· 2288 2335 "openssl-probe", 2289 2336 "openssl-sys", 2290 2337 "schannel", 2291 - "security-framework", 2338 + "security-framework 2.11.1", 2292 2339 "security-framework-sys", 2293 2340 "tempfile", 2294 2341 ] ··· 3496 3543 ] 3497 3544 3498 3545 [[package]] 3546 + name = "security-framework" 3547 + version = "3.3.0" 3548 + source = "registry+https://github.com/rust-lang/crates.io-index" 3549 + checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" 3550 + dependencies = [ 3551 + "bitflags 2.9.3", 3552 + "core-foundation 0.10.1", 3553 + "core-foundation-sys", 3554 + "libc", 3555 + "security-framework-sys", 3556 + ] 3557 + 3558 + [[package]] 3499 3559 name = "security-framework-sys" 3500 3560 version = "2.14.0" 3501 3561 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3858 3918 version = "0.11.1" 3859 3919 source = "registry+https://github.com/rust-lang/crates.io-index" 3860 3920 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 3921 + 3922 + [[package]] 3923 + name = "strum" 3924 + version = "0.27.2" 3925 + source = "registry+https://github.com/rust-lang/crates.io-index" 3926 + checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" 3927 + dependencies = [ 3928 + "strum_macros", 3929 + ] 3930 + 3931 + [[package]] 3932 + name = "strum_macros" 3933 + version = "0.27.2" 3934 + source = "registry+https://github.com/rust-lang/crates.io-index" 3935 + checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" 3936 + dependencies = [ 3937 + "heck 0.5.0", 3938 + "proc-macro2", 3939 + "quote", 3940 + "syn 2.0.106", 3941 + ] 3861 3942 3862 3943 [[package]] 3863 3944 name = "swift-rs" ··· 5673 5754 "quote", 5674 5755 "syn 2.0.106", 5675 5756 "synstructure", 5757 + ] 5758 + 5759 + [[package]] 5760 + name = "zeroize" 5761 + version = "1.8.1" 5762 + source = "registry+https://github.com/rust-lang/crates.io-index" 5763 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 5764 + dependencies = [ 5765 + "zeroize_derive", 5766 + ] 5767 + 5768 + [[package]] 5769 + name = "zeroize_derive" 5770 + version = "1.4.2" 5771 + source = "registry+https://github.com/rust-lang/crates.io-index" 5772 + checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 5773 + dependencies = [ 5774 + "proc-macro2", 5775 + "quote", 5776 + "syn 2.0.106", 5676 5777 ] 5677 5778 5678 5779 [[package]]
+2
src-tauri/Cargo.toml
··· 28 28 futures-util = "0.3.31" 29 29 tokio = "1.47.1" 30 30 dasp = { version = "0.11.0", features = ["all"] } 31 + keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "sync-secret-service"] } 32 + strum = {version = "0.27.2", features = ["derive"] } 31 33 32 34 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 33 35 tauri-plugin-positioner = "2"
+14 -8
src-tauri/src/cartesia/client.rs
··· 1 1 use std::sync::Arc; 2 - use tauri::{async_runtime::RwLock, Url}; 2 + use tauri::{http::HeaderValue, Url}; 3 3 use tokio::net::TcpStream; 4 4 use tokio_tungstenite::{ 5 5 connect_async, tungstenite::client::IntoClientRequest, MaybeTlsStream, WebSocketStream, 6 6 }; 7 7 8 + use crate::secrets::{SecretName, SecretsManager}; 9 + 8 10 #[derive(Clone)] 9 11 pub struct CartesiaClient { 10 - api_key: Arc<RwLock<String>>, 12 + secrets_manager: Arc<SecretsManager>, 11 13 } 12 14 13 15 impl CartesiaClient { 14 - pub fn new(api_key: String) -> Self { 15 - Self { 16 - api_key: Arc::new(RwLock::new(api_key)), 17 - } 16 + pub fn new(secrets_manager: Arc<SecretsManager>) -> Self { 17 + Self { secrets_manager } 18 18 } 19 19 20 20 pub async fn open_stt_connection(&self) -> WebSocketStream<MaybeTlsStream<TcpStream>> { 21 - let api_key = self.api_key.read().await.clone(); 22 21 let mut request = Url::parse_with_params( 23 22 "wss://api.cartesia.ai/stt/websocket", 24 23 &[ ··· 33 32 .expect("failed to instantiate STT WebSocket request"); 34 33 35 34 let headers = request.headers_mut(); 35 + let api_key = self 36 + .secrets_manager 37 + .get_secret(SecretName::CartesiaApiKey) 38 + .expect("failed to retrieve API key"); 36 39 37 - headers.insert("X-API-Key", api_key.parse().unwrap()); 40 + headers.insert( 41 + "X-API-Key", 42 + HeaderValue::from_str(api_key.as_str()).expect("could not convert key to header value"), 43 + ); 38 44 headers.insert("Cartesia-Version", "2025-04-16".parse().unwrap()); 39 45 40 46 let (stream, _) = connect_async(request)
+4 -1
src-tauri/src/lib.rs
··· 1 1 use crate::state::AppState; 2 - use tauri::async_runtime::Mutex; 3 2 use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; 4 3 use tauri_plugin_positioner::{Position, WindowExt}; 5 4 use tauri_plugin_window_state::{StateFlags, WindowExt as StateWindowExt}; 6 5 7 6 mod cartesia; 8 7 mod devices; 8 + mod secrets; 9 9 mod state; 10 10 11 11 // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ ··· 67 67 .invoke_handler(tauri::generate_handler![ 68 68 cartesia::commands::start_stt, 69 69 cartesia::commands::stop_stt, 70 + secrets::commands::has_secret, 71 + secrets::commands::set_secret, 72 + secrets::commands::delete_secret, 70 73 ]) 71 74 .run(tauri::generate_context!()) 72 75 .expect("error while running tauri application");
+61
src-tauri/src/secrets/commands.rs
··· 1 + use crate::{secrets::SecretName, state::AppState}; 2 + use std::str::FromStr; 3 + 4 + #[tauri::command] 5 + pub async fn has_secret(state: tauri::State<'_, AppState>, name: String) -> Result<bool, ()> { 6 + match SecretName::from_str(&name) { 7 + Ok(name) => { 8 + let res = state 9 + .secrets_manager 10 + .has_secret(name) 11 + .expect("failed to check credential existence"); 12 + 13 + Ok(res) 14 + } 15 + Err(err) => { 16 + eprintln!("invalid credential name: {}", err); 17 + 18 + Ok(false) 19 + } 20 + } 21 + } 22 + 23 + #[tauri::command] 24 + pub fn set_secret( 25 + state: tauri::State<'_, AppState>, 26 + name: String, 27 + value: String, 28 + ) -> Result<(), ()> { 29 + match SecretName::from_str(&name) { 30 + Ok(name) => { 31 + state 32 + .secrets_manager 33 + .set_secret(name, value) 34 + .expect("failed to set credential"); 35 + 36 + Ok(()) 37 + } 38 + Err(err) => { 39 + eprintln!("failed to decode secret name: {}", err); 40 + Err(()) 41 + } 42 + } 43 + } 44 + 45 + #[tauri::command] 46 + pub fn delete_secret(state: tauri::State<'_, AppState>, name: String) -> Result<(), ()> { 47 + match SecretName::from_str(&name) { 48 + Ok(name) => { 49 + state 50 + .secrets_manager 51 + .delete_secret(name) 52 + .expect("failed to delete credential"); 53 + 54 + Ok(()) 55 + } 56 + Err(err) => { 57 + eprintln!("failed to decode secret name: {}", err); 58 + Err(()) 59 + } 60 + } 61 + }
+46
src-tauri/src/secrets/mod.rs
··· 1 + use keyring::{Entry, Error}; 2 + use strum::{Display, EnumString}; 3 + 4 + pub mod commands; 5 + 6 + #[derive(Debug, PartialEq, Display, EnumString)] 7 + pub enum SecretName { 8 + #[strum(to_string = "cartesia_api_key")] 9 + CartesiaApiKey, 10 + } 11 + 12 + const SERVICE_NAME: &'static str = "miwiwi"; 13 + 14 + pub struct SecretsManager {} 15 + impl SecretsManager { 16 + pub fn new() -> Self { 17 + Self {} 18 + } 19 + 20 + pub fn get_secret(&self, name: SecretName) -> Result<String, Error> { 21 + let entry = Entry::new(SERVICE_NAME, &name.to_string())?; 22 + let cred = entry.get_password()?; 23 + 24 + Ok(cred) 25 + } 26 + 27 + pub fn set_secret(&self, name: SecretName, value: String) -> Result<(), Error> { 28 + let entry = Entry::new(SERVICE_NAME, &name.to_string())?; 29 + 30 + entry.set_password(&value) 31 + } 32 + 33 + pub fn delete_secret(&self, name: SecretName) -> Result<(), Error> { 34 + let entry = Entry::new(SERVICE_NAME, &name.to_string())?; 35 + 36 + entry.delete_credential() 37 + } 38 + 39 + pub fn has_secret(&self, name: SecretName) -> Result<bool, Error> { 40 + match self.get_secret(name) { 41 + Ok(_) => Ok(true), 42 + Err(Error::NoEntry) => Ok(false), 43 + Err(err) => Err(err), 44 + } 45 + } 46 + }
+5 -2
src-tauri/src/state.rs
··· 3 3 use crate::{ 4 4 cartesia::{client::CartesiaClient, stt::SttManager}, 5 5 devices::{input::InputDeviceManager, output::OutputDeviceManager, types::AudioDeviceError}, 6 + secrets::SecretsManager, 6 7 }; 7 8 8 9 pub struct AppState { 9 10 pub cartesia_client: Arc<CartesiaClient>, 10 11 pub stt_manager: Arc<SttManager>, 12 + pub secrets_manager: Arc<SecretsManager>, 11 13 pub input_device_manager: Arc<InputDeviceManager>, 12 14 pub output_device_manager: Arc<OutputDeviceManager>, 13 15 } ··· 16 18 pub fn new() -> Result<Self, AudioDeviceError> { 17 19 let input_device_manager = Arc::new(InputDeviceManager::new()?); 18 20 let output_device_manager = Arc::new(OutputDeviceManager::new()?); 19 - 20 - let cartesia_client = Arc::new(CartesiaClient::new("TODO".into())); 21 + let secrets_manager = Arc::new(SecretsManager::new()); 22 + let cartesia_client = Arc::new(CartesiaClient::new(secrets_manager.clone())); 21 23 let stt_manager = Arc::new(SttManager::new( 22 24 cartesia_client.clone(), 23 25 input_device_manager.clone(), ··· 26 28 Ok(AppState { 27 29 input_device_manager, 28 30 output_device_manager, 31 + secrets_manager, 29 32 cartesia_client, 30 33 stt_manager, 31 34 })
+29 -2
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { invoke } from "@tauri-apps/api/core" 3 + 4 + let keyInvocation = $state(invoke("has_secret", { name: "cartesia_api_key"})) 5 + let tempCredential = $state("") 6 + 7 + async function deleteKey() { 8 + await invoke("delete_secret", { name: "cartesia_api_key"}) 9 + keyInvocation = invoke("has_secret", { name: "cartesia_api_key"}) 10 + } 3 11 </script> 4 12 13 + {#await keyInvocation} 14 + <p>checking for cred...</p> 15 + {:then hasKey} 16 + {#if hasKey} 17 + <p>key has been set</p> 18 + <button onclick={deleteKey}>delete key</button> 19 + {:else} 20 + <p>no key yet</p> 21 + <input placeholder="my key..." bind:value={tempCredential} /> 22 + <button onclick={async () => { 23 + await invoke("set_secret", { name: "cartesia_api_key", value: tempCredential}) 24 + 25 + keyInvocation = invoke("has_secret", { name: "cartesia_api_key"}) 26 + }}>Save API key</button> 27 + {/if} 28 + {:catch err} 29 + <p>{err}</p> 30 + {/await} 31 + 5 32 <main>miwiwi</main> 6 - <button rel="button" on:click={() => invoke("start_stt")}>Start STT</button> 7 - <button rel="button" on:click={() => invoke("stop_stt")}>Stop STT</button> 33 + <button onclick={() => invoke("start_stt")}>Start STT</button> 34 + <button onclick={() => invoke("stop_stt")}>Stop STT</button> 8 35