diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-17 21:43:52 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-17 21:43:52 +0300 |
| commit | 915e435679a5539a75acaffe1324dc45f0f76364 (patch) | |
| tree | bfc256ea61751f7d01397dadf350b40398c43138 | |
| parent | f6477f82dc79d17e9ee3193c81dca2db884a7119 (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.go | 172 | ||||
| -rw-r--r-- | internal/gui/generator.go | 20 | ||||
| -rw-r--r-- | internal/gui/navigation.go | 9 | ||||
| -rwxr-xr-x | test_all_ui_elements.sh | 33 | ||||
| -rwxr-xr-x | test_image_prompt_persistence.sh | 31 | ||||
| -rwxr-xr-x | test_no_mixup.sh | 23 | ||||
| -rwxr-xr-x | test_phonetic_gui.sh | 21 | ||||
| -rwxr-xr-x | test_prompt_persistence.sh | 26 | ||||
| -rwxr-xr-x | test_translation_edit.sh | 17 | ||||
| -rwxr-xr-x | test_translation_fix.sh | 23 |
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 |
