| 1 | use serde::{Deserialize, Serialize}; |
| 2 | use std::fs; |
| 3 | use std::path::PathBuf; |
| 4 | use tauri::{AppHandle, Manager}; |
| 5 | |
| 6 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 7 | #[serde(default)] |
| 8 | pub struct EnhanceProfile { |
| 9 | pub name: String, |
| 10 | |
| 11 | pub provider: String, |
| 12 | pub model: String, |
| 13 | |
| 14 | pub base_url: String, |
| 15 | |
| 16 | pub api_key_env: String, |
| 17 | |
| 18 | pub cli_command: String, |
| 19 | pub instructions: String, |
| 20 | } |
| 21 | |
| 22 | impl Default for EnhanceProfile { |
| 23 | fn default() -> Self { |
| 24 | Self { |
| 25 | name: "cleanup".into(), |
| 26 | provider: "anthropic".into(), |
| 27 | model: "claude-opus-4-8".into(), |
| 28 | base_url: "".into(), |
| 29 | api_key_env: "".into(), |
| 30 | cli_command: "".into(), |
| 31 | instructions: DEFAULT_INSTRUCTIONS.into(), |
| 32 | } |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 37 | #[serde(default)] |
| 38 | pub struct EnhanceSettings { |
| 39 | pub enabled: bool, |
| 40 | pub active_profile: String, |
| 41 | pub profiles: Vec<EnhanceProfile>, |
| 42 | } |
| 43 | |
| 44 | impl Default for EnhanceSettings { |
| 45 | fn default() -> Self { |
| 46 | Self { |
| 47 | enabled: false, |
| 48 | active_profile: "cleanup".into(), |
| 49 | profiles: vec![ |
| 50 | EnhanceProfile::default(), |
| 51 | EnhanceProfile { |
| 52 | name: "email".into(), |
| 53 | instructions: EMAIL_INSTRUCTIONS.into(), |
| 54 | ..Default::default() |
| 55 | }, |
| 56 | EnhanceProfile { |
| 57 | name: "bullets".into(), |
| 58 | instructions: BULLET_INSTRUCTIONS.into(), |
| 59 | ..Default::default() |
| 60 | }, |
| 61 | ], |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | impl EnhanceSettings { |
| 67 | |
| 68 | pub fn active(&self) -> Option<&EnhanceProfile> { |
| 69 | self.profiles |
| 70 | .iter() |
| 71 | .find(|p| p.name == self.active_profile) |
| 72 | .or_else(|| self.profiles.first()) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | pub const DEFAULT_INSTRUCTIONS: &str = "Fix punctuation, capitalization, filler words, and obvious transcription mistakes. Keep the speaker's wording and tone unchanged."; |
| 77 | |
| 78 | pub const EMAIL_INSTRUCTIONS: &str = "Rewrite the transcript as a clear, friendly, professional email body with short paragraphs. Only include a greeting or sign-off if the speaker dictated one."; |
| 79 | |
| 80 | pub const BULLET_INSTRUCTIONS: &str = "Convert the transcript into terse bullet points, one idea per line, keeping every concrete detail the speaker mentioned."; |
| 81 | |
| 82 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 83 | #[serde(default)] |
| 84 | pub struct Settings { |
| 85 | pub hotkey: String, |
| 86 | |
| 87 | pub activation: String, |
| 88 | |
| 89 | pub injection_mode: String, |
| 90 | pub restore_clipboard: bool, |
| 91 | |
| 92 | pub dictation_model: String, |
| 93 | |
| 94 | pub language: String, |
| 95 | pub enhance: EnhanceSettings, |
| 96 | } |
| 97 | |
| 98 | impl Default for Settings { |
| 99 | fn default() -> Self { |
| 100 | Self { |
| 101 | hotkey: "Ctrl+Alt+Space".into(), |
| 102 | activation: "toggle".into(), |
| 103 | injection_mode: "paste".into(), |
| 104 | restore_clipboard: true, |
| 105 | dictation_model: "base.en".into(), |
| 106 | language: "auto".into(), |
| 107 | enhance: EnhanceSettings::default(), |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | fn settings_path(app: &AppHandle) -> Result<PathBuf, String> { |
| 113 | let dir = app |
| 114 | .path() |
| 115 | .app_config_dir() |
| 116 | .map_err(|e| format!("no config dir: {e}"))?; |
| 117 | fs::create_dir_all(&dir).map_err(|e| format!("create config dir: {e}"))?; |
| 118 | Ok(dir.join("settings.json")) |
| 119 | } |
| 120 | |
| 121 | pub fn load(app: &AppHandle) -> Settings { |
| 122 | let Ok(path) = settings_path(app) else { |
| 123 | return Settings::default(); |
| 124 | }; |
| 125 | let Some(mut value) = fs::read_to_string(path) |
| 126 | .ok() |
| 127 | .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok()) |
| 128 | else { |
| 129 | return Settings::default(); |
| 130 | }; |
| 131 | migrate_legacy_enhance(&mut value); |
| 132 | serde_json::from_value(value).unwrap_or_default() |
| 133 | } |
| 134 | |
| 135 | |
| 136 | fn migrate_legacy_enhance(value: &mut serde_json::Value) { |
| 137 | let Some(enhance) = value.get_mut("enhance") else { |
| 138 | return; |
| 139 | }; |
| 140 | if enhance.get("profiles").is_some() || enhance.get("provider").is_none() { |
| 141 | return; |
| 142 | } |
| 143 | let enabled = enhance.get("enabled").cloned().unwrap_or(false.into()); |
| 144 | let mut profile = enhance.clone(); |
| 145 | if let Some(obj) = profile.as_object_mut() { |
| 146 | obj.remove("enabled"); |
| 147 | obj.insert("name".into(), "default".into()); |
| 148 | } |
| 149 | *enhance = serde_json::json!({ |
| 150 | "enabled": enabled, |
| 151 | "active_profile": "default", |
| 152 | "profiles": [profile], |
| 153 | }); |
| 154 | } |
| 155 | |
| 156 | pub fn save(app: &AppHandle, settings: &Settings) -> Result<(), String> { |
| 157 | let path = settings_path(app)?; |
| 158 | let raw = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?; |
| 159 | fs::write(path, raw).map_err(|e| format!("write settings: {e}")) |
| 160 | } |