summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-17 21:43:52 +0300
committerPaul Buetow <paul@buetow.org>2025-07-17 21:43:52 +0300
commit915e435679a5539a75acaffe1324dc45f0f76364 (patch)
treebfc256ea61751f7d01397dadf350b40398c43138
parentf6477f82dc79d17e9ee3193c81dca2db884a7119 (diff)
fix: prevent UI element mix-ups during concurrent processing and rapid navigation
- Fixed one-way translation bug: English to Bulgarian no longer triggers reverse translation - Prevent mix-ups when rapidly entering new words while previous words are processing - Ensure all UI elements update only for their associated job: * Bulgarian/English text fields protected by job ID checks * Phonetic info saved to disk immediately and UI updates restricted * Audio file associations protected from background job interference * Image prompts saved immediately and UI updates controlled - Add mutex protection for thread-safe UI updates - Disconnect from background jobs when user starts typing new input - Save all generated data to disk immediately, not just on UI update - Improve navigation to properly load all associated data from disk This ensures correct flashcard generation even with rapid word entry and navigation. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--internal/gui/app.go172
-rw-r--r--internal/gui/generator.go20
-rw-r--r--internal/gui/navigation.go9
-rwxr-xr-xtest_all_ui_elements.sh33
-rwxr-xr-xtest_image_prompt_persistence.sh31
-rwxr-xr-xtest_no_mixup.sh23
-rwxr-xr-xtest_phonetic_gui.sh21
-rwxr-xr-xtest_prompt_persistence.sh26
-rwxr-xr-xtest_translation_edit.sh17
-rwxr-xr-xtest_translation_fix.sh23
10 files changed, 318 insertions, 57 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go
index 45eec63..50c9746 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -155,11 +155,28 @@ func (a *Application) setupUI() {
a.wordInput = widget.NewEntry()
a.wordInput.SetPlaceHolder("Bulgarian word...")
a.wordInput.OnSubmitted = func(string) { a.onSubmit() }
+ a.wordInput.OnChanged = func(text string) {
+ // When user starts typing a new word, disconnect from any previous job
+ // to prevent mix-ups with background processing
+ a.mu.Lock()
+ if a.currentJobID != 0 && text != a.currentWord {
+ a.currentJobID = 0
+ }
+ a.mu.Unlock()
+ }
// Create translation entry
a.translationEntry = widget.NewEntry()
a.translationEntry.SetPlaceHolder("English translation...")
a.translationEntry.OnChanged = func(text string) {
+ // When user starts typing in translation field, disconnect from any previous job
+ // to prevent mix-ups with background processing
+ a.mu.Lock()
+ if a.currentJobID != 0 && a.currentTranslation != text {
+ a.currentJobID = 0
+ }
+ a.mu.Unlock()
+
a.currentTranslation = text
// Save the updated translation immediately
a.saveTranslation()
@@ -364,6 +381,7 @@ func (a *Application) onSubmit() {
a.currentWord = bulgarian
// Save the translation immediately
a.saveTranslation()
+ needsTranslation = false // We've already done the translation, don't translate back
} else if translationDirection == "bg-to-en" {
// Handle Bulgarian to English translation immediately
a.updateStatus(fmt.Sprintf("Translating '%s' to English...", bulgarianText))
@@ -426,10 +444,23 @@ func (a *Application) generateMaterials(word string) {
})
return
}
- a.currentTranslation = translation
- fyne.Do(func() {
- a.translationEntry.SetText(translation)
- })
+ // Only update if this word is still the current word
+ a.mu.Lock()
+ if a.currentWord == word {
+ a.currentTranslation = translation
+ fyne.Do(func() {
+ a.translationEntry.SetText(translation)
+ })
+ }
+ a.mu.Unlock()
+
+ // Save translation to disk regardless
+ if translation != "" {
+ filename := sanitizeFilename(word)
+ translationFile := filepath.Join(a.config.OutputDir, fmt.Sprintf("%s_translation.txt", filename))
+ content := fmt.Sprintf("%s = %s\n", word, translation)
+ os.WriteFile(translationFile, []byte(content), 0644)
+ }
}
// Generate audio
@@ -447,10 +478,16 @@ func (a *Application) generateMaterials(word string) {
})
return
}
- a.currentAudioFile = audioFile
- fyne.Do(func() {
- a.audioPlayer.SetAudioFile(audioFile)
- })
+
+ // Only update UI if this word is still the current word
+ a.mu.Lock()
+ if a.currentWord == word {
+ a.currentAudioFile = audioFile
+ fyne.Do(func() {
+ a.audioPlayer.SetAudioFile(audioFile)
+ })
+ }
+ a.mu.Unlock()
// Generate images with custom prompt if provided
fyne.Do(func() {
@@ -477,11 +514,17 @@ func (a *Application) generateMaterials(word string) {
})
return
}
+
+ // Only update UI if this word is still the current word
if imageFile != "" {
- a.currentImage = imageFile
- fyne.Do(func() {
- a.imageDisplay.SetImages([]string{imageFile})
- })
+ a.mu.Lock()
+ if a.currentWord == word {
+ a.currentImage = imageFile
+ fyne.Do(func() {
+ a.imageDisplay.SetImages([]string{imageFile})
+ })
+ }
+ a.mu.Unlock()
}
// Enable action buttons
@@ -536,6 +579,15 @@ func (a *Application) onKeepAndContinue() {
a.clearUI()
a.wordInput.SetText("")
a.translationEntry.SetText("")
+
+ // Clear current state to prevent mix-ups with background jobs
+ a.mu.Lock()
+ a.currentWord = ""
+ a.currentTranslation = ""
+ a.currentAudioFile = ""
+ a.currentImage = ""
+ a.mu.Unlock()
+
a.wordInput.FocusGained() // Focus input for next word
// Hide progress bar if it was showing
@@ -748,6 +800,7 @@ func (a *Application) clearUI() {
a.imageDisplay.Clear()
a.audioPlayer.Clear()
// Don't clear the word input or translation entry - they should stay populated
+ // Clear the image prompt entry - it will be loaded from disk if available
a.imagePromptEntry.SetText("")
a.phoneticDisplay.SetText("Phonetic information will appear here...")
a.setActionButtonsEnabled(false)
@@ -766,10 +819,14 @@ func (a *Application) processNextInQueue() {
return
}
- // Set current job
+ // Set current job and clear any previous state
a.mu.Lock()
a.currentJobID = job.ID
a.currentWord = job.Word
+ // Clear previous file associations to prevent mix-ups
+ a.currentTranslation = ""
+ a.currentAudioFile = ""
+ a.currentImage = ""
a.mu.Unlock()
// Clear UI for new word
@@ -810,6 +867,14 @@ func (a *Application) processWordJob(job *WordJob) {
translation = job.Translation
}
+ // Save translation to disk immediately for this specific word
+ if translation != "" {
+ filename := sanitizeFilename(job.Word)
+ translationFile := filepath.Join(a.config.OutputDir, fmt.Sprintf("%s_translation.txt", filename))
+ content := fmt.Sprintf("%s = %s\n", job.Word, translation)
+ os.WriteFile(translationFile, []byte(content), 0644)
+ }
+
// Update UI with translation immediately if this is still the current job
a.mu.Lock()
if a.currentJobID == job.ID && translation != "" {
@@ -836,13 +901,18 @@ func (a *Application) processWordJob(job *WordJob) {
phoneticInfo = "Failed to fetch phonetic information"
}
+ // Save phonetic info to disk immediately for this specific word
+ if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" {
+ filename := sanitizeFilename(job.Word)
+ phoneticFile := filepath.Join(a.config.OutputDir, fmt.Sprintf("%s_phonetic.txt", filename))
+ os.WriteFile(phoneticFile, []byte(phoneticInfo), 0644)
+ }
+
// Update UI with phonetic info if this is still the current job
a.mu.Lock()
if a.currentJobID == job.ID {
fyne.Do(func() {
a.phoneticDisplay.SetText(phoneticInfo)
- // Save phonetic info to file
- a.savePhoneticInfo()
})
}
a.mu.Unlock()
@@ -867,15 +937,19 @@ func (a *Application) processWordJob(job *WordJob) {
// Update UI with audio immediately if this is still the current job
a.mu.Lock()
- if a.currentJobID == job.ID {
+ isCurrentJob := a.currentJobID == job.ID
+ if isCurrentJob {
a.currentAudioFile = audioFile
+ }
+ a.mu.Unlock()
+
+ if isCurrentJob {
fyne.Do(func() {
a.audioPlayer.SetAudioFile(audioFile)
// Enable audio-related actions
a.regenerateAudioBtn.Enable()
})
}
- a.mu.Unlock()
// Generate images
fyne.Do(func() {
@@ -906,13 +980,17 @@ func (a *Application) processWordJob(job *WordJob) {
// Update UI with results if this is still the current job
a.mu.Lock()
- if a.currentJobID == job.ID {
+ isCurrentJob = a.currentJobID == job.ID
+ if isCurrentJob {
a.currentTranslation = translation
a.currentAudioFile = audioFile
if imageFile != "" {
a.currentImage = imageFile
}
-
+ }
+ a.mu.Unlock()
+
+ if isCurrentJob {
fyne.Do(func() {
a.translationEntry.SetText(translation)
if imageFile != "" {
@@ -924,7 +1002,6 @@ func (a *Application) processWordJob(job *WordJob) {
a.updateStatus(fmt.Sprintf("Completed: %s", job.Word))
})
}
- a.mu.Unlock()
// Finish this job
a.finishCurrentJob()
@@ -970,39 +1047,17 @@ func (a *Application) onJobComplete(job *WordJob) {
if job.Status == StatusCompleted {
a.updateNavigation()
- // Check if the completed job is for the currently displayed word
- // Only update UI if the current word is still empty (waiting for this job)
- if job.Word == a.currentWord && job.ID != a.currentJobID {
- // Check if the UI is still empty/waiting for content
- hasContent := a.currentAudioFile != "" || a.currentImage != ""
-
- if !hasContent {
- // Update the UI with the completed results since it's still waiting
- // Update each component individually to show progress
- if job.Translation != "" && a.currentTranslation == "" {
- a.currentTranslation = job.Translation
- a.translationEntry.SetText(job.Translation)
- }
- if job.AudioFile != "" && a.currentAudioFile == "" {
- a.currentAudioFile = job.AudioFile
- a.audioPlayer.SetAudioFile(job.AudioFile)
- a.regenerateAudioBtn.Enable()
- }
- if job.ImageFile != "" && a.currentImage == "" {
- a.currentImage = job.ImageFile
- a.imageDisplay.SetImages([]string{job.ImageFile})
- a.regenerateImageBtn.Enable()
- }
-
- // Enable all action buttons since we now have complete content
- a.setActionButtonsEnabled(true)
- a.updateStatus(fmt.Sprintf("Processing completed: %s", job.Word))
- } else {
- // Word already has content, just show notification
- a.updateStatus(fmt.Sprintf("Background processing completed: %s", job.Word))
- }
- } else if job.ID != a.currentJobID {
- // Show a subtle notification for other background completions
+ // Only show status updates, don't update UI for background jobs
+ // This prevents mix-ups when user has moved on to a new word
+ a.mu.Lock()
+ isCurrentJob := job.ID == a.currentJobID
+ a.mu.Unlock()
+
+ if isCurrentJob {
+ // This is still the current job, UI update is already handled in processWordJob
+ a.updateStatus(fmt.Sprintf("Processing completed: %s", job.Word))
+ } else {
+ // This is a background job that completed
a.updateStatus(fmt.Sprintf("Background processing completed: %s", job.Word))
}
}
@@ -1166,6 +1221,17 @@ func (a *Application) savePhoneticInfo() {
}
}
+// savePhoneticInfoForWord saves the phonetic information for a specific word
+func (a *Application) savePhoneticInfoForWord(word, phoneticText string) {
+ if word != "" && phoneticText != "" &&
+ phoneticText != "Failed to fetch phonetic information" &&
+ phoneticText != "Phonetic information will appear here..." {
+ filename := sanitizeFilename(word)
+ phoneticFile := filepath.Join(a.config.OutputDir, fmt.Sprintf("%s_phonetic.txt", filename))
+ os.WriteFile(phoneticFile, []byte(phoneticText), 0644)
+ }
+}
+
// loadPhoneticInfo loads phonetic information from a file if it exists
func (a *Application) loadPhoneticInfo(word string) {
filename := sanitizeFilename(word)
diff --git a/internal/gui/generator.go b/internal/gui/generator.go
index 6d51359..d26bb81 100644
--- a/internal/gui/generator.go
+++ b/internal/gui/generator.go
@@ -176,14 +176,26 @@ func (a *Application) generateImagesWithPrompt(word string, customPrompt string,
return "", err
}
- // If using OpenAI, get the last used prompt and update the UI
+ // If using OpenAI, get the last used prompt
if a.config.ImageProvider == "openai" {
if openaiClient, ok := searcher.(*image.OpenAIClient); ok {
usedPrompt := openaiClient.GetLastPrompt()
if usedPrompt != "" {
- fyne.Do(func() {
- a.imagePromptEntry.SetText(usedPrompt)
- })
+ // Save the prompt to disk immediately for this word
+ filename := sanitizeFilename(word)
+ promptFile := filepath.Join(a.config.OutputDir, fmt.Sprintf("%s_prompt.txt", filename))
+ os.WriteFile(promptFile, []byte(usedPrompt), 0644)
+
+ // Only update UI if this word is still the current word
+ a.mu.Lock()
+ isCurrentWord := a.currentWord == word
+ a.mu.Unlock()
+
+ if isCurrentWord {
+ fyne.Do(func() {
+ a.imagePromptEntry.SetText(usedPrompt)
+ })
+ }
}
}
}
diff --git a/internal/gui/navigation.go b/internal/gui/navigation.go
index 62a326e..1466466 100644
--- a/internal/gui/navigation.go
+++ b/internal/gui/navigation.go
@@ -189,6 +189,15 @@ func (a *Application) loadWordByIndex(index int) {
}
// Load phonetic info from disk if it exists
a.loadPhoneticInfo(word)
+
+ // Load image prompt from disk if it exists
+ sanitized := sanitizeFilename(word)
+ promptFile := filepath.Join(a.config.OutputDir, fmt.Sprintf("%s_prompt.txt", sanitized))
+ if data, err := os.ReadFile(promptFile); err == nil {
+ prompt := strings.TrimSpace(string(data))
+ a.imagePromptEntry.SetText(prompt)
+ }
+
a.updateStatus(fmt.Sprintf("Loaded from queue: %s", word))
})
diff --git a/test_all_ui_elements.sh b/test_all_ui_elements.sh
new file mode 100755
index 0000000..ddaa627
--- /dev/null
+++ b/test_all_ui_elements.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Test script to verify all UI elements are preserved during rapid navigation
+
+echo "Starting totalrecall GUI to test all UI element preservation..."
+echo ""
+echo "Test procedure:"
+echo "1. Enter multiple words rapidly in different ways:"
+echo " - Word 1: Enter 'apple' in English field only"
+echo " - Word 2: Enter 'ΠΊΠΎΡ‚ΠΊΠ°' in Bulgarian field only"
+echo " - Word 3: Enter 'ΠΊΡƒΡ‡Π΅' in Bulgarian with custom prompt 'brown dog running'"
+echo " - Word 4: Enter 'car' in English with custom prompt 'red sports car'"
+echo ""
+echo "2. While processing is happening, rapidly navigate using arrow keys"
+echo "3. Verify that for each word:"
+echo " - Bulgarian text stays correct"
+echo " - English translation stays correct"
+echo " - Custom image prompts are preserved"
+echo " - Phonetic information is displayed correctly"
+echo " - Audio files play the correct word"
+echo ""
+echo "4. Check the anki_cards folder for:"
+echo " - *_translation.txt files with correct translations"
+echo " - *_prompt.txt files with correct prompts"
+echo " - *_phonetic.txt files with correct phonetic info"
+echo " - Audio files that match the word names"
+echo ""
+echo "5. Restart the app and navigate through the words again"
+echo "6. Verify all data is correctly loaded from disk"
+echo ""
+echo "Press Ctrl+C to exit when testing is complete."
+echo ""
+
+./totalrecall gui \ No newline at end of file
diff --git a/test_image_prompt_persistence.sh b/test_image_prompt_persistence.sh
new file mode 100755
index 0000000..adc311a
--- /dev/null
+++ b/test_image_prompt_persistence.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Test script to verify image prompt persistence functionality
+
+echo "Testing image prompt persistence in totalrecall GUI..."
+echo ""
+echo "Test scenarios:"
+echo ""
+echo "1. Create card with custom prompt:"
+echo " - Enter 'ΠΊΠΎΡ‚ΠΊΠ°' in Bulgarian field"
+echo " - Enter custom prompt: 'cute fluffy cat playing with yarn'"
+echo " - Generate the card"
+echo " - The prompt is automatically saved"
+echo ""
+echo "2. Navigate away and back:"
+echo " - Use navigation arrows to go to another word"
+echo " - Navigate back to 'ΠΊΠΎΡ‚ΠΊΠ°'"
+echo " - The custom prompt should be restored"
+echo ""
+echo "3. Regenerate image:"
+echo " - Click 'Regenerate Image'"
+echo " - The saved prompt will be used automatically"
+echo ""
+echo "4. Edit and save prompt:"
+echo " - Modify the prompt text"
+echo " - Changes are saved automatically"
+echo " - Regenerate to see the new prompt in action"
+echo ""
+echo "Press Ctrl+C to exit when testing is complete."
+echo ""
+
+./totalrecall gui \ No newline at end of file
diff --git a/test_no_mixup.sh b/test_no_mixup.sh
new file mode 100755
index 0000000..863977d
--- /dev/null
+++ b/test_no_mixup.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# Test script to verify that rapid word entry doesn't cause mix-ups
+
+echo "Starting totalrecall GUI to test rapid word entry..."
+echo ""
+echo "Test procedure:"
+echo "1. Enter a word (e.g., 'ябълка') and click Generate"
+echo "2. While it's still processing, immediately enter a new word (e.g., 'ΠΊΠΎΡ‚ΠΊΠ°')"
+echo "3. Click Generate for the second word"
+echo "4. Verify that:"
+echo " - The first word's image/audio appears correctly when it completes"
+echo " - The second word's image/audio appears correctly when it completes"
+echo " - No mix-ups occur between the two words"
+echo "5. Check the anki_cards folder to ensure files are correctly named"
+echo ""
+echo "Additional tests:"
+echo "- Try entering 'rocket launcher' in English, then quickly enter another word"
+echo "- Verify the translation doesn't get mixed up"
+echo ""
+echo "Press Ctrl+C to exit when testing is complete."
+echo ""
+
+./totalrecall gui \ No newline at end of file
diff --git a/test_phonetic_gui.sh b/test_phonetic_gui.sh
new file mode 100755
index 0000000..0ceaf6b
--- /dev/null
+++ b/test_phonetic_gui.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Test script for the phonetic information GUI feature
+
+echo "Testing TotalRecall GUI with phonetic information feature..."
+echo ""
+echo "Features to test:"
+echo "1. Enter Bulgarian words like: ябълка (apple), ΠΊΠΎΡ‚ΠΊΠ° (cat), ΠΊΡƒΡ‡Π΅ (dog)"
+echo "2. Phonetic info shows detailed IPA with pronunciation examples for EACH letter"
+echo "3. Examples compare to English sounds (e.g., '/a/ like in father')"
+echo "4. Phonetic info is saved automatically and persists after restart"
+echo "5. Use arrow keys or prev/next buttons to navigate between cards"
+echo "6. Info fetches concurrently with audio/image for faster processing"
+echo ""
+echo "The phonetic text area is located between the image section and audio controls."
+echo ""
+echo "Press Enter to start the GUI..."
+read
+
+# Run the GUI
+./totalrecall gui \ No newline at end of file
diff --git a/test_prompt_persistence.sh b/test_prompt_persistence.sh
new file mode 100755
index 0000000..33c4b7e
--- /dev/null
+++ b/test_prompt_persistence.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Test script to verify that image prompts are preserved during rapid navigation
+
+echo "Starting totalrecall GUI to test prompt persistence..."
+echo ""
+echo "Test procedure:"
+echo "1. Enter multiple words quickly with custom prompts:"
+echo " - Word 1: 'ябълка' with prompt 'red apple on wooden table'"
+echo " - Word 2: 'ΠΊΠΎΡ‚ΠΊΠ°' with prompt 'orange cat sleeping on sofa'"
+echo " - Word 3: 'ΠΊΡƒΡ‡Π΅' with prompt 'golden retriever playing in park'"
+echo ""
+echo "2. While images are still generating, navigate rapidly using arrow keys"
+echo "3. Verify that:"
+echo " - Each word retains its correct custom prompt"
+echo " - Prompts don't get mixed up between words"
+echo " - Prompts are saved to disk (*_prompt.txt files)"
+echo ""
+echo "4. After all processing completes, navigate through words again"
+echo "5. Verify prompts are correctly loaded from disk"
+echo ""
+echo "Check the anki_cards folder for *_prompt.txt files"
+echo ""
+echo "Press Ctrl+C to exit when testing is complete."
+echo ""
+
+./totalrecall gui \ No newline at end of file
diff --git a/test_translation_edit.sh b/test_translation_edit.sh
new file mode 100755
index 0000000..79b501a
--- /dev/null
+++ b/test_translation_edit.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Test script to verify the translation editing functionality
+
+echo "Starting totalrecall GUI with translation editing feature..."
+echo ""
+echo "Test instructions:"
+echo "1. Enter a Bulgarian word (e.g., 'ябълка')"
+echo "2. Click 'Generate' or press 'g'"
+echo "3. Wait for translation to appear"
+echo "4. Try editing the translation in the text field"
+echo "5. Navigate away and back to verify the translation was saved"
+echo "6. The edited translation should persist"
+echo ""
+echo "Press Ctrl+C to exit when testing is complete."
+echo ""
+
+./totalrecall gui \ No newline at end of file
diff --git a/test_translation_fix.sh b/test_translation_fix.sh
new file mode 100755
index 0000000..8be4aeb
--- /dev/null
+++ b/test_translation_fix.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+# Test script to verify the translation fix
+
+echo "Testing totalrecall translation fix..."
+echo "======================================="
+echo ""
+echo "This test verifies that when a Bulgarian word is translated to English,"
+echo "the English translation is used for image generation without re-translating."
+echo ""
+echo "To test manually:"
+echo "1. Run: ./totalrecall"
+echo "2. Enter a Bulgarian word (e.g., ябълка)"
+echo "3. Wait for it to be translated to English (e.g., apple)"
+echo "4. Optionally edit the English translation"
+echo "5. Click Generate or press Enter"
+echo "6. Watch the console output - it should show:"
+echo " 'Using provided translation: ябълка -> apple'"
+echo " instead of translating again"
+echo ""
+echo "Expected behavior:"
+echo "- The image generation uses the translation shown in the UI"
+echo "- No additional translation happens during image generation"
+echo "- Console shows 'Using provided translation' message" \ No newline at end of file