summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-16 21:27:08 +0300
committerPaul Buetow <paul@buetow.org>2025-07-16 21:27:08 +0300
commit6b0b4c9ce28b88ab0abbff03c5f367d89ba97c89 (patch)
treefc102068a7e41c8042bdf2d17674405e715da8be
parentd46669426aa6b0ece71d0d05d0b6f2966686b17a (diff)
fix: update button labels to show lowercase hotkeys and improve export location
- 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 <noreply@anthropic.com>
-rw-r--r--internal/gui/app.go23
-rw-r--r--internal/gui/audio_player.go8
-rw-r--r--internal/gui/navigation.go2
-rw-r--r--internal/image/openai.go104
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
+}