Skip to content

Commit 6f3b15f

Browse files
committed
fix: resolve menu bar updates and AI enhancement issues
- Add immediate tray menu updates when model selection changes - Fix default enhancement prompt to preserve natural speech format - Add error feedback in recording pill when enhancement fails - Implement event-driven API key removal with proper UI state sync - Remove duplicate state updates and cache clearing calls - Fix backend validation to allow empty provider for deselection These changes ensure proper state synchronization across the app using event-driven architecture, consistent with existing patterns in SettingsContext.
1 parent 2339b43 commit 6f3b15f

File tree

8 files changed

+122
-81
lines changed

8 files changed

+122
-81
lines changed

src-tauri/src/ai/prompts.rs

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,66 +83,67 @@ pub fn build_enhancement_prompt(
8383
prompt
8484
}
8585

86-
const DEFAULT_PROMPT: &str = r#"THEN clean up this voice transcription to create professional, readable text while preserving the speaker's intended meaning.
86+
const DEFAULT_PROMPT: &str = r#"THEN clean up this voice transcription like a high-quality dictation tool would - fix errors while keeping the natural speech flow.
8787
8888
APPLY THESE CORRECTIONS:
8989
9090
1. REMOVE speech artifacts:
91-
- Filler words: um, uh, ah, er, hmm, like (when filler), you know (when filler), basically (when redundant), actually (when redundant)
91+
- Filler words: um, uh, ah, er, hmm, like (when filler), you know (when filler)
9292
- False starts and incomplete thoughts
9393
- Unintentional word repetitions and stutters: "I I think", "that that's"
9494
- Verbal backspacing: "wait", "scratch that", "let me rephrase"
9595
9696
2. FIX common errors:
97-
- Homophones: there/their/they're, to/too/two, your/you're, its/it's, then/than, here/hear, write/right
97+
- Homophones: there/their/they're, to/too/two, your/you're, its/it's, then/than
9898
- Grammar: subject-verb agreement, article usage (a/an/the)
99-
- Technical terms: "java script" → "JavaScript", "type script" → "TypeScript", "react js" → "React.js"
99+
- Technical terms (fix misheard/misspelled tech words):
100+
* "java script" → "JavaScript", "type script" → "TypeScript", "react js" → "React.js"
101+
* "fortend/frontent/front and" → "frontend"
102+
* "backing/back and/beck end" → "backend"
103+
* "jason/jayson" → "JSON", "A P I" → "API"
104+
* Common programming terms that sound similar
100105
- Contractions: "dont" → "don't", "wont" → "won't", "cant" → "can't"
101106
- Word boundaries: "alot" → "a lot", "incase" → "in case"
102107
103-
3. FORMAT properly:
108+
3. ADD proper formatting:
104109
- Capitalize sentence beginnings and proper nouns (names, places, brands)
105110
- Add punctuation based on speech patterns and context
106111
- Numbers: "twenty twenty five" → "2025", "one hundred" → "100"
107-
- Times: "two thirty PM" → "2:30 PM", "three o'clock" → "3:00"
108-
- Dates: "january first" → "January 1st", "the fifth of march" → "March 5th"
112+
- Times: "two thirty PM" → "2:30 PM"
113+
- Dates: "january first" → "January 1st"
109114
- Split run-on sentences at natural break points
110-
- Format lists when detected: "first... second..." → "1) ... 2) ..."
111115
112-
4. HANDLE spoken punctuation (when clearly commands):
113-
- "comma" → , (when spoken as command)
114-
- "period" or "full stop" → .
116+
4. HANDLE explicit dictation commands:
117+
- "period" or "full stop" → . (only when clearly dictating)
118+
- "comma" → , (only when clearly dictating)
115119
- "question mark" → ?
116-
- "exclamation point" → !
117-
- "quote/unquote" → "..."
118-
- "open/close parenthesis" → ()
119-
- "colon" → :
120-
- "semicolon" → ;
121-
122-
5. PRESERVE:
123-
- Technical jargon and domain-specific terms
124-
- Intentional emphasis through repetition
125-
- Natural conversational tone
126-
- Quoted speech or dialogue
127-
- Acronyms spoken as letters (FBI, API, URL)
120+
- "new paragraph" → create paragraph break
121+
- Email addresses: "john at gmail dot com" → "john@gmail.com"
122+
123+
5. KEEP the natural flow:
124+
- Don't restructure into lists or bullet points
125+
- Keep the conversational sequence intact
126+
- Preserve the speaker's tone and style
127+
- Don't make casual speech overly formal
128+
- Maintain original sentence connections
128129
129130
EXAMPLES:
130-
"um their going too the store at two thirty PM comma and there buying to apples period"
131-
→ "They're going to the store at 2:30 PM, and they're buying two apples."
131+
"um their going too the store at two thirty PM and there buying to apples"
132+
→ "They're going to the store at 2:30 PM and they're buying two apples."
132133
133134
"the java script function returns uh jason data with a two hundred status"
134135
→ "The JavaScript function returns JSON data with a 200 status."
135136
136137
"first we need milk second bread third eggs"
137-
→ "First, we need: 1) milk, 2) bread, 3) eggs."
138+
→ "First, we need milk, second bread, third eggs."
138139
139-
"can you send it to john at gmail dot com question mark"
140+
"can you send it to john at gmail dot com"
140141
→ "Can you send it to john@gmail.com?"
141142
142143
"I I think that that's the last no wait the least important one"
143144
→ "I think that's the least important one."
144145
145-
Return ONLY the cleaned text."#;
146+
Return ONLY the cleaned text as natural dictation output."#;
146147

147148
// Thin transformation layer for Prompts preset
148149
const PROMPTS_TRANSFORM: &str = r#"FINALLY, transform the cleaned text into a well-structured AI prompt:

src-tauri/src/commands/ai.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,19 @@ pub async fn clear_ai_api_key_cache(
257257
_app: tauri::AppHandle,
258258
provider: String,
259259
) -> Result<(), String> {
260-
validate_provider_name(&provider)?;
260+
// Skip validation if provider is empty (happens when clearing selection)
261+
if !provider.is_empty() {
262+
validate_provider_name(&provider)?;
263+
}
261264

262265
let mut cache = API_KEY_CACHE
263266
.lock()
264267
.map_err(|_| "Failed to access cache".to_string())?;
265-
cache.remove(&format!("ai_api_key_{}", provider));
266-
267-
log::info!("API key cache cleared for provider: {}", provider);
268+
269+
if !provider.is_empty() {
270+
cache.remove(&format!("ai_api_key_{}", provider));
271+
log::info!("API key cache cleared for provider: {}", provider);
272+
}
268273

269274
Ok(())
270275
}
@@ -286,7 +291,10 @@ pub async fn update_ai_settings(
286291
model: String,
287292
app: tauri::AppHandle,
288293
) -> Result<(), String> {
289-
validate_provider_name(&provider)?;
294+
// Allow empty provider and model for deselection
295+
if !provider.is_empty() {
296+
validate_provider_name(&provider)?;
297+
}
290298

291299
// Allow empty model (for deselection) but validate if not empty
292300
if !model.is_empty() {

src-tauri/src/commands/audio.rs

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,40 +1013,37 @@ pub async fn stop_recording(
10131013
enhanced
10141014
}
10151015
Err(e) => {
1016-
// Emit enhancing failed event
1016+
log::warn!("AI enhancement failed, using original text: {}", e);
1017+
1018+
// Check error type and create appropriate message
1019+
let error_message = e.to_string();
1020+
let user_message = if error_message.contains("400") || error_message.contains("Bad Request") {
1021+
"Enhancement failed: Missing or invalid API key"
1022+
} else if error_message.contains("401") || error_message.contains("Unauthorized") {
1023+
"Enhancement failed: Invalid API key"
1024+
} else if error_message.contains("429") {
1025+
"Enhancement failed: Rate limit exceeded"
1026+
} else if error_message.contains("network") || error_message.contains("connection") {
1027+
"Enhancement failed: Network error"
1028+
} else {
1029+
"Enhancement failed: Using original text"
1030+
};
1031+
1032+
// Emit enhancing failed event with error message to pill
10171033
let _ = emit_to_window(
10181034
&app_for_process,
10191035
"pill",
10201036
"enhancing-failed",
1021-
(),
1037+
user_message,
10221038
);
1023-
log::warn!("AI enhancement failed, using original text: {}", e);
10241039

1025-
// Check if it's an authentication error and notify frontend
1026-
let error_message = e.to_string();
1027-
if error_message.contains("401")
1028-
|| error_message.contains("Unauthorized")
1029-
{
1040+
// Also notify main window for settings update if needed
1041+
if error_message.contains("400") || error_message.contains("401") || error_message.contains("Bad Request") || error_message.contains("Unauthorized") {
10301042
let _ = emit_to_window(
10311043
&app_for_process,
10321044
"main",
10331045
"ai-enhancement-auth-error",
1034-
"Invalid API key. Please check your AI settings.",
1035-
);
1036-
} else if error_message.contains("429") {
1037-
let _ = emit_to_window(
1038-
&app_for_process,
1039-
"main",
1040-
"ai-enhancement-error",
1041-
"Rate limit exceeded. Please try again later.",
1042-
);
1043-
} else {
1044-
// Generic enhancement error
1045-
let _ = emit_to_window(
1046-
&app_for_process,
1047-
"main",
1048-
"ai-enhancement-error",
1049-
"AI enhancement failed. Using original text.",
1046+
"Please check your AI API key in settings.",
10501047
);
10511048
}
10521049

src-tauri/src/commands/settings.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,18 @@ pub async fn save_settings(app: AppHandle, settings: Settings) -> Result<(), Str
145145

146146
store.save().map_err(|e| e.to_string())?;
147147

148-
// Preload new model if it changed
148+
// Preload new model and update tray menu if model changed
149149
if !settings.current_model.is_empty() && old_model != settings.current_model {
150150
use crate::commands::model::preload_model;
151151
use tauri::async_runtime::RwLock as AsyncRwLock;
152152

153153
log::info!(
154-
"Model changed from '{}' to '{}', preloading new model",
154+
"Model changed from '{}' to '{}', preloading new model and updating tray menu",
155155
old_model,
156156
settings.current_model
157157
);
158158

159+
// Preload the new model
159160
let app_clone = app.clone();
160161
let model_name = settings.current_model.clone();
161162
tokio::spawn(async move {
@@ -166,6 +167,12 @@ pub async fn save_settings(app: AppHandle, settings: Settings) -> Result<(), Str
166167
Err(e) => log::warn!("Failed to preload new model: {}", e),
167168
}
168169
});
170+
171+
// Update the tray menu to reflect the new selection
172+
if let Err(e) = update_tray_menu(app.clone()).await {
173+
log::warn!("Failed to update tray menu after model change: {}", e);
174+
// Don't fail the whole operation if tray update fails
175+
}
169176
}
170177

171178
Ok(())

src/components/RecordingPill.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,11 @@ export function RecordingPill() {
156156
);
157157

158158
unlisteners.push(
159-
listen("enhancing-failed", () => {
160-
console.log("RecordingPill: Received enhancing-failed event");
159+
listen<string>("enhancing-failed", (event) => {
160+
console.log("RecordingPill: Received enhancing-failed event", event.payload);
161161
setIsEnhancing(false);
162+
// Show error message for 4 seconds
163+
setFeedbackWithTimeout(event.payload || "Enhancement failed", 4000);
162164
})
163165
);
164166

src/components/sections/EnhancementsSection.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,41 @@ export function EnhancementsSection() {
187187
}
188188
});
189189

190+
// Listen for API key remove events
191+
const unlistenApiKeyRemoved = listen<{ provider: string }>('api-key-removed', async (event) => {
192+
console.log('[AI Settings] API key removed for provider:', event.payload.provider);
193+
194+
// Update local state immediately
195+
setProviderApiKeys(prev => ({ ...prev, [event.payload.provider]: false }));
196+
197+
// Check if the removed key was for the currently selected model
198+
const selectedModel = models.find(m => m.id === aiSettings.model);
199+
if (selectedModel && selectedModel.provider === event.payload.provider) {
200+
console.log('[AI Settings] Clearing model selection for removed API key');
201+
// Clear the model selection and disable AI
202+
setAISettings(prev => ({
203+
...prev,
204+
enabled: false,
205+
provider: "",
206+
model: "",
207+
hasApiKey: false
208+
}));
209+
210+
// Update backend to clear the selection
211+
try {
212+
await invoke("update_ai_settings", {
213+
enabled: false,
214+
provider: "",
215+
model: ""
216+
});
217+
} catch (error) {
218+
console.error('Failed to update backend settings:', error);
219+
}
220+
}
221+
});
222+
190223
return () => {
191-
Promise.all([unlistenReady, unlistenApiKey]).then(fns => {
224+
Promise.all([unlistenReady, unlistenApiKey, unlistenApiKeyRemoved]).then(fns => {
192225
fns.forEach(fn => fn());
193226
});
194227
};
@@ -275,21 +308,11 @@ export function EnhancementsSection() {
275308
const handleRemoveApiKey = async (provider: string) => {
276309
try {
277310
console.log(`[AI Settings] Removing API key for provider: ${provider}`);
311+
312+
// Remove the key (this also clears backend cache and emits the event)
313+
// The 'api-key-removed' event listener will handle all UI updates
278314
await removeApiKey(provider);
279-
280-
// If the removed key was for the currently selected model, deselect it
281-
const selectedModel = models.find(m => m.id === aiSettings.model);
282-
if (selectedModel && selectedModel.provider === provider) {
283-
console.log(`[AI Settings] Deselecting model ${selectedModel.id} due to API key removal`);
284-
// Deselect model and disable AI enhancement
285-
await invoke("update_ai_settings", {
286-
enabled: false,
287-
provider: aiSettings.provider,
288-
model: "" // Clear model selection
289-
});
290-
}
291-
292-
await loadAISettings();
315+
293316
toast.success("API key removed");
294317
} catch (error) {
295318
console.error(`[AI Settings] Failed to remove API key:`, error);
@@ -332,7 +355,7 @@ export function EnhancementsSection() {
332355
key={model.id}
333356
model={model}
334357
hasApiKey={providerApiKeys[model.provider] || false}
335-
isSelected={aiSettings.model === model.id}
358+
isSelected={aiSettings.model === model.id && providerApiKeys[model.provider]}
336359
onSetupApiKey={() => handleSetupApiKey(model.provider)}
337360
onSelect={() => handleModelSelect(model.id, model.provider)}
338361
onRemoveApiKey={() => handleRemoveApiKey(model.provider)}

src/components/tabs/ModelsTab.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ export function ModelsTab() {
3131

3232
// If deleted model was the current one, clear selection in settings
3333
if (settings?.current_model === modelName) {
34-
await saveSettings({ ...settings, current_model: "" });
34+
await saveSettings({ current_model: "" });
3535
}
3636
},
3737
[deleteModel, settings]
3838
);
3939

4040
// Save settings
4141
const saveSettings = useCallback(
42-
async (newSettings: AppSettings) => {
42+
async (updates: Partial<AppSettings>) => {
4343
try {
44-
await updateSettings(newSettings);
44+
await updateSettings(updates);
4545
} catch (error) {
4646
console.error("Failed to save settings:", error);
4747
}
@@ -88,7 +88,7 @@ export function ModelsTab() {
8888
onCancelDownload={cancelDownload}
8989
onSelect={async (modelName) => {
9090
if (settings) {
91-
await saveSettings({ ...settings, current_model: modelName });
91+
await saveSettings({ current_model: modelName });
9292
}
9393
}}
9494
/>

src/utils/keyring.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export const removeApiKey = async (provider: string): Promise<void> => {
6868
await invoke('clear_ai_api_key_cache', { provider });
6969

7070
console.log(`[Keyring] API key removed for ${provider}`);
71+
72+
// Emit event to notify that API key was removed
73+
await emit('api-key-removed', { provider });
7174
};
7275

7376
// Load all API keys to backend cache (for app startup)

0 commit comments

Comments
 (0)