From 6b0b4c9ce28b88ab0abbff03c5f367d89ba97c89 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 16 Jul 2025 21:27:08 +0300 Subject: fix: update button labels to show lowercase hotkeys and improve export location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change all button labels to show lowercase letters (g, n, i, a, r, d, p) - Update delete confirmation dialog to show lowercase y/n - Set default export location to anki_cards directory - Add note about CSV needing to be in same directory as media files - Fix prompt generation to remove Bulgarian word reference 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/gui/app.go | 23 +++++++--- internal/gui/audio_player.go | 8 ++-- internal/gui/navigation.go | 2 +- internal/image/openai.go | 104 +++++++++++++++++++++---------------------- 4 files changed, 72 insertions(+), 65 deletions(-) diff --git a/internal/gui/app.go b/internal/gui/app.go index 3783eb9..836ebca 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -157,7 +157,7 @@ func (a *Application) setupUI() { a.wordInput.SetPlaceHolder("Enter Bulgarian word...") a.wordInput.OnSubmitted = func(string) { a.onSubmit() } - a.submitButton = widget.NewButton("Generate (G)", a.onSubmit) + a.submitButton = widget.NewButton("Generate (g)", a.onSubmit) a.prevWordBtn = widget.NewButton("◀ Prev (←)", a.onPrevWord) a.nextWordBtn = widget.NewButton("Next (→) ▶", a.onNextWord) @@ -203,11 +203,11 @@ func (a *Application) setupUI() { ) // Create action buttons - a.keepButton = widget.NewButton("New Word (N)", a.onKeepAndContinue) - a.regenerateImageBtn = widget.NewButton("Regenerate Image (I)", a.onRegenerateImage) - a.regenerateAudioBtn = widget.NewButton("Regenerate Audio (A)", a.onRegenerateAudio) - a.regenerateAllBtn = widget.NewButton("Regenerate All (R)", a.onRegenerateAll) - a.deleteButton = widget.NewButton("Delete (D)", a.onDelete) + a.keepButton = widget.NewButton("New Word (n)", a.onKeepAndContinue) + a.regenerateImageBtn = widget.NewButton("Regenerate Image (i)", a.onRegenerateImage) + a.regenerateAudioBtn = widget.NewButton("Regenerate Audio (a)", a.onRegenerateAudio) + a.regenerateAllBtn = widget.NewButton("Regenerate All (r)", a.onRegenerateAll) + a.deleteButton = widget.NewButton("Delete (d)", a.onDelete) a.deleteButton.Importance = widget.DangerImportance // Initially disable action buttons @@ -561,12 +561,21 @@ func (a *Application) onExportToAnki() { } dialog.ShowInformation("Export Complete", - fmt.Sprintf("Exported %d cards to:\n%s", len(a.savedCards), outputPath), + fmt.Sprintf("Exported %d cards to:\n%s\n\nNote: The CSV file should be in the same directory as your media files (%s) for Anki import to work correctly.", + len(a.savedCards), outputPath, a.config.OutputDir), a.window) }, a.window) saveDialog.SetFileName("anki_import.csv") saveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".csv"})) + + // Try to set the default location to the anki_cards directory + if uri, err := storage.ParseURI("file://" + a.config.OutputDir); err == nil { + if listableURI, ok := uri.(fyne.ListableURI); ok { + saveDialog.SetLocation(listableURI) + } + } + saveDialog.Show() } diff --git a/internal/gui/audio_player.go b/internal/gui/audio_player.go index 2d4b2da..f36eddf 100644 --- a/internal/gui/audio_player.go +++ b/internal/gui/audio_player.go @@ -31,7 +31,7 @@ func NewAudioPlayer() *AudioPlayer { p := &AudioPlayer{} // Create controls - p.playButton = widget.NewButton("▶ Play (P)", p.onPlay) + p.playButton = widget.NewButton("▶ Play (p)", p.onPlay) p.stopButton = widget.NewButton("■ Stop", p.onStop) p.statusLabel = widget.NewLabel("No audio loaded") @@ -98,7 +98,7 @@ func (p *AudioPlayer) onPlay() { } p.isPlaying = true - p.playButton.SetText("⏸ Pause (P)") + p.playButton.SetText("⏸ Pause (p)") p.stopButton.Enable() p.statusLabel.SetText("Playing: " + filepath.Base(p.audioFile)) } @@ -111,7 +111,7 @@ func (p *AudioPlayer) onStop() { } p.isPlaying = false - p.playButton.SetText("▶ Play (P)") + p.playButton.SetText("▶ Play (p)") p.stopButton.Disable() p.statusLabel.SetText("Stopped: " + filepath.Base(p.audioFile)) } @@ -164,7 +164,7 @@ func (p *AudioPlayer) startPlayback() error { // Playback finished normally fyne.Do(func() { p.isPlaying = false - p.playButton.SetText("▶ Play (P)") + p.playButton.SetText("▶ Play (p)") p.stopButton.Disable() p.statusLabel.SetText("Finished: " + filepath.Base(p.audioFile)) }) diff --git a/internal/gui/navigation.go b/internal/gui/navigation.go index e59b817..05bf988 100644 --- a/internal/gui/navigation.go +++ b/internal/gui/navigation.go @@ -294,7 +294,7 @@ func (a *Application) onDelete() { } // Create custom confirmation dialog with keyboard support - message := fmt.Sprintf("Delete all files for '%s'?\n\nPress Y to confirm or N to cancel", a.currentWord) + message := fmt.Sprintf("Delete all files for '%s'?\n\nPress y to confirm or n to cancel", a.currentWord) confirmDialog := dialog.NewConfirm("Delete Word", message, func(confirm bool) { a.deleteConfirming = false if confirm { diff --git a/internal/image/openai.go b/internal/image/openai.go index add1c96..2fbb043 100644 --- a/internal/image/openai.go +++ b/internal/image/openai.go @@ -12,7 +12,7 @@ import ( "path/filepath" "strings" "time" - + "github.com/sashabaranov/go-openai" ) @@ -46,9 +46,9 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient { // Return nil client that will fail on operations return &OpenAIClient{} } - + client := openai.NewClient(config.APIKey) - + // Set defaults if config.Model == "" { config.Model = "dall-e-3" @@ -65,7 +65,7 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient { if config.CacheDir == "" { config.CacheDir = "./.image_cache" } - + oc := &OpenAIClient{ client: client, apiKey: config.APIKey, @@ -76,12 +76,12 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient { cacheDir: config.CacheDir, enableCache: config.EnableCache, } - + // Create cache directory if caching is enabled if oc.enableCache && oc.cacheDir != "" { os.MkdirAll(oc.cacheDir, 0755) } - + return oc } @@ -94,7 +94,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Message: "OpenAI API key not configured", } } - + // Check cache first if c.enableCache { cacheFile := c.getCacheFilePath(opts.Query) @@ -114,7 +114,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc return []SearchResult{result}, nil } } - + // Translate Bulgarian word to English for better results translatedWord, err := c.translateBulgarianToEnglish(ctx, opts.Query) if err != nil { @@ -122,7 +122,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc fmt.Printf("Translation failed: %v, using original word\n", err) translatedWord = opts.Query } - + // Create prompt - use custom if provided, otherwise generate educational prompt var prompt string if opts.CustomPrompt != "" { @@ -131,14 +131,14 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc } else { prompt = c.createEducationalPrompt(opts.Query, translatedWord) } - + // Store the prompt for attribution c.lastPrompt = prompt - + // Log the prompt to stdout for debugging fmt.Printf("OpenAI Image Generation Prompt: %s\n", prompt) fmt.Printf("OpenAI Image Generation: Using model '%s' with size '%s'\n", c.model, c.size) - + // Create the image generation request req := openai.ImageRequest{ Prompt: prompt, @@ -147,13 +147,13 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc ResponseFormat: openai.CreateImageResponseFormatURL, N: 1, } - + // Add model-specific parameters if c.model == "dall-e-3" { req.Quality = c.quality req.Style = c.style } - + // Generate the image resp, err := c.client.CreateImage(ctx, req) if err != nil { @@ -163,7 +163,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Message: fmt.Sprintf("Failed to generate image: %v", err), } } - + if len(resp.Data) == 0 { return nil, &SearchError{ Provider: "openai", @@ -171,10 +171,10 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Message: "No image generated", } } - + // Get the generated image URL imageURL := resp.Data[0].URL - + // Download and cache the image if caching is enabled if c.enableCache { cacheFile := c.getCacheFilePath(opts.Query) @@ -184,7 +184,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc } // Continue even if caching fails } - + // Create result result := SearchResult{ ID: c.generateImageID(opts.Query), @@ -196,7 +196,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Attribution: "Generated by OpenAI DALL-E", Source: "openai", } - + return []SearchResult{result}, nil } @@ -210,23 +210,23 @@ func (c *OpenAIClient) Download(ctx context.Context, url string) (io.ReadCloser, } return file, nil } - + // Otherwise download from URL req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } - + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } - + if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) } - + return resp.Body, nil } @@ -261,16 +261,15 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation if creativeStyle := c.getCreativeStyleFromOpenAI(context.Background(), englishTranslation); creativeStyle != "" { fmt.Printf(" Using OpenAI-suggested style: %s\n", creativeStyle) return fmt.Sprintf( - "Generate a %s of: %s. "+ - "This is for the Bulgarian word '%s' which means %s. "+ + "Generate %s of: %s. "+ "The image should be educational and suitable for language learning flashcards. "+ "Requirements: single main subject, plain background, clear and recognizable. "+ "IMPORTANT: No text whatsoever. Do not include any words, letters, typography, labels, captions, or writing of any kind. Image only, without any text elements.", - creativeStyle, englishTranslation, bulgarianWord, englishTranslation, + creativeStyle, englishTranslation, ) } } - + // Define different art styles for variety (42 styles total) styles := []string{ // Original styles (1-10) @@ -284,12 +283,12 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "oil painting, classical art style", "paper cut art, layered craft style", "isometric illustration, technical drawing style", - + // Requested styles (11-13) "superhero comic book style, dynamic action pose, bold colors, Marvel/DC aesthetic", "super-realistic person practicing yoga, serene wellness photography", "cute illustration with cats interacting with the subject, whimsical cat-themed", - + // Additional artistic styles (14-25) "impressionist painting style, Monet-inspired brushstrokes", "art nouveau style, decorative organic forms, Mucha-inspired", @@ -303,7 +302,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "mosaic tile art style, Byzantine-inspired patterns", "art deco style, geometric patterns, 1920s aesthetic", "surrealist style, Salvador Dali inspired dreamlike quality", - + // Photography styles (26-32) "macro photography style, extreme close-up detail", "vintage polaroid photograph, retro instant camera aesthetic", @@ -312,7 +311,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "underwater photography style, ethereal aquatic atmosphere", "aerial drone photography, bird's eye view perspective", "long exposure photography, motion blur effects", - + // Modern digital styles (33-42) "vaporwave aesthetic, 80s-90s retro digital art", "low poly 3D art style, geometric simplified forms", @@ -325,19 +324,18 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "origami paper folding art style, geometric paper craft", "chalk art style, sidewalk drawing aesthetic", } - + // Select a random style selectedStyle := styles[rand.Intn(len(styles))] fmt.Printf(" Using image style: %s\n", selectedStyle) - + // Create a simple, clear prompt for educational images return fmt.Sprintf( "Generate a %s of: %s. "+ - "This is for the Bulgarian word '%s' which means %s. "+ "The image should be educational and suitable for language learning flashcards. "+ "Requirements: single main subject, plain background, clear and recognizable. "+ "IMPORTANT: No text whatsoever. Do not include any words, letters, typography, labels, captions, or writing of any kind. Image only, without any text elements.", - selectedStyle, englishTranslation, bulgarianWord, englishTranslation, + selectedStyle, englishTranslation, ) } @@ -345,7 +343,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation func (c *OpenAIClient) translateBulgarianToEnglish(ctx context.Context, word string) (string, error) { // Use OpenAI chat completion to translate fmt.Printf("OpenAI Translation: Using model 'gpt-4o-mini' to translate '%s'\n", word) - + req := openai.ChatCompletionRequest{ Model: openai.GPT4oMini, Messages: []openai.ChatCompletionMessage{ @@ -357,19 +355,19 @@ func (c *OpenAIClient) translateBulgarianToEnglish(ctx context.Context, word str Temperature: 0.3, // Lower temperature for more consistent translations MaxTokens: 50, } - + resp, err := c.client.CreateChatCompletion(ctx, req) if err != nil { return "", fmt.Errorf("translation failed: %w", err) } - + if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" { return "", fmt.Errorf("no translation received") } - + translation := strings.TrimSpace(resp.Choices[0].Message.Content) fmt.Printf("Translated '%s' to '%s'\n", word, translation) - + return translation, nil } @@ -383,11 +381,11 @@ func (c *OpenAIClient) getCacheFilePath(word string) string { h.Write([]byte(c.quality)) h.Write([]byte(c.style)) hash := hex.EncodeToString(h.Sum(nil)) - + // Use first 2 chars as subdirectory for better file system performance subdir := hash[:2] filename := hash[2:] + ".png" - + return filepath.Join(c.cacheDir, subdir, filename) } @@ -398,21 +396,21 @@ func (c *OpenAIClient) downloadAndCache(ctx context.Context, url, cacheFile stri if err := os.MkdirAll(dir, 0755); err != nil { return err } - + // Download the image resp, err := c.Download(ctx, url) if err != nil { return err } defer resp.Close() - + // Create the cache file out, err := os.Create(cacheFile) if err != nil { return err } defer out.Close() - + // Copy the data _, err = io.Copy(out, resp) return err @@ -466,37 +464,37 @@ func (c *OpenAIClient) getSizeHeight() int { // getCreativeStyleFromOpenAI asks OpenAI for a creative photo style suggestion func (c *OpenAIClient) getCreativeStyleFromOpenAI(ctx context.Context, subject string) string { fmt.Printf(" Asking OpenAI for creative style suggestion for '%s'...\n", subject) - + req := openai.ChatCompletionRequest{ Model: openai.GPT4oMini, Messages: []openai.ChatCompletionMessage{ { - Role: openai.ChatMessageRoleSystem, + Role: openai.ChatMessageRoleSystem, Content: "You are a creative art director. Suggest unique, interesting photo/art styles for educational flashcard images. Be creative and varied. Respond with ONLY the style description, nothing else. Keep it concise (max 15 words).", }, { - Role: openai.ChatMessageRoleUser, + Role: openai.ChatMessageRoleUser, Content: fmt.Sprintf("Suggest a creative visual style for an educational image of: %s", subject), }, }, Temperature: 0.9, // Higher temperature for more creativity MaxTokens: 30, } - + resp, err := c.client.CreateChatCompletion(ctx, req) if err != nil { fmt.Printf(" Failed to get creative style: %v\n", err) return "" } - + if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" { return "" } - + style := strings.TrimSpace(resp.Choices[0].Message.Content) // Remove any trailing punctuation style = strings.TrimSuffix(style, ".") style = strings.TrimSuffix(style, "!") - + return style -} \ No newline at end of file +} -- cgit v1.2.3