diff --git a/cmd/hyprvoice/main.go b/cmd/hyprvoice/main.go index 80c8cb3..1032bd7 100644 --- a/cmd/hyprvoice/main.go +++ b/cmd/hyprvoice/main.go @@ -124,7 +124,9 @@ func cancelCmd() *cobra.Command { } func configureCmd() *cobra.Command { - return &cobra.Command{ + var onboarding bool + + cmd := &cobra.Command{ Use: "configure", Short: "Interactive configuration setup", Long: `Interactive configuration wizard for hyprvoice. @@ -134,12 +136,16 @@ This will guide you through setting up: - LLM post-processing - Text injection and notification preferences`, RunE: func(cmd *cobra.Command, args []string) error { - return runConfigure() + return runConfigure(onboarding) }, } + + cmd.Flags().BoolVar(&onboarding, "onboarding", false, "Run the guided onboarding wizard") + + return cmd } -func runConfigure() error { +func runConfigure(onboarding bool) error { // Load existing config or create default cfg, err := config.Load() if err != nil { @@ -147,7 +153,7 @@ func runConfigure() error { } // Run TUI wizard - result, err := tui.Run(cfg) + result, err := tui.Run(cfg, onboarding) if err != nil { return fmt.Errorf("configuration wizard error: %w", err) } diff --git a/internal/tui/configure.go b/internal/tui/configure.go index 4b795ea..d52db4e 100644 --- a/internal/tui/configure.go +++ b/internal/tui/configure.go @@ -43,11 +43,19 @@ const ( ) // Run starts the TUI configuration wizard -func Run(existingConfig *config.Config) (*ConfigureResult, error) { - if existingConfig != nil && hasUserChanges(existingConfig) { +// If onboarding is true, forces the guided wizard flow even if config exists +func Run(existingConfig *config.Config, onboarding bool) (*ConfigureResult, error) { + if !onboarding && existingConfig != nil && hasUserChanges(existingConfig) { return runEditExisting(existingConfig) } - return runFreshInstall(existingConfig) + + result, err := runFreshInstall(existingConfig) + if err != nil || result.Cancelled { + return result, err + } + + // wizard done, transition to menu for review/save + return runEditExisting(result.Config) } // hasUserChanges detects if config has user modifications @@ -92,7 +100,7 @@ func runEditExisting(cfg *config.Config) (*ConfigureResult, error) { return &ConfigureResult{Cancelled: true}, nil case SectionProviders: - if err := editProviders(cfg); err != nil { + if err := editProviders(cfg, false); err != nil { continue } configuredProviders = getConfiguredProviders(cfg) @@ -131,7 +139,7 @@ func runEditExisting(cfg *config.Config) (*ConfigureResult, error) { } case SectionAdvanced: - if err := editAdvanced(cfg); err != nil { + if err := editAdvanced(cfg, false); err != nil { continue } } diff --git a/internal/tui/configure_advanced.go b/internal/tui/configure_advanced.go index 1cb5b49..5abcf08 100644 --- a/internal/tui/configure_advanced.go +++ b/internal/tui/configure_advanced.go @@ -19,12 +19,16 @@ const ( ) // editAdvanced handles the advanced settings submenu -func editAdvanced(cfg *config.Config) error { +func editAdvanced(cfg *config.Config, onboarding bool) error { + exitLabel := "Done" + if onboarding { + exitLabel = "Next" + } for { options := []huh.Option[AdvancedSection]{ huh.NewOption(formatAdvancedRecordingLabel(cfg), AdvancedRecording), huh.NewOption(formatAdvancedInjectionTimeoutLabel(cfg), AdvancedInjectionTimeout), - huh.NewOption("Back to Main Menu", AdvancedBack), + huh.NewOption(exitLabel, AdvancedBack), } var selected AdvancedSection diff --git a/internal/tui/configure_llm.go b/internal/tui/configure_llm.go index 5abe25d..5000ce3 100644 --- a/internal/tui/configure_llm.go +++ b/internal/tui/configure_llm.go @@ -291,154 +291,3 @@ func selectPostProcessingOptions(current config.LLMPostProcessingConfig) (config return result, nil } - -func configureLLM(configuredProviders []string, cfg *config.Config) (bool, string, string, config.LLMPostProcessingConfig, config.LLMCustomPromptConfig, error) { - var llmProviders []string - for _, name := range configuredProviders { - p := provider.GetProvider(name) - if p != nil && p.SupportsLLM() { - llmProviders = append(llmProviders, name) - } - } - - postProcessing := config.LLMPostProcessingConfig{ - RemoveStutters: true, - AddPunctuation: true, - FixGrammar: true, - RemoveFillerWords: true, - } - customPrompt := config.LLMCustomPromptConfig{ - Enabled: false, - Prompt: "", - } - - if len(llmProviders) == 0 { - return false, "", "", postProcessing, customPrompt, nil - } - - var enableLLM bool = true - enableForm := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title("Enable LLM Post-Processing? (Recommended)"). - Description("LLM improves transcription by fixing grammar, removing stutters, and cleaning up text"). - Affirmative("Yes (Recommended)"). - Negative("No"). - Value(&enableLLM), - ), - ).WithTheme(getTheme()) - - if err := enableForm.Run(); err != nil { - return false, "", "", postProcessing, customPrompt, err - } - - if !enableLLM { - return false, "", "", postProcessing, customPrompt, nil - } - - var llmOptions []huh.Option[string] - for _, name := range llmProviders { - p := provider.GetProvider(name) - if p != nil { - switch name { - case "openai": - llmOptions = append(llmOptions, huh.NewOption("OpenAI GPT", "openai")) - case "groq": - llmOptions = append(llmOptions, huh.NewOption("Groq Llama (fast)", "groq")) - } - } - } - - var selectedProvider string - if cfg.LLM.Provider != "" { - selectedProvider = cfg.LLM.Provider - } else if len(llmOptions) > 0 { - selectedProvider = llmOptions[0].Value - } - - providerForm := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("LLM Provider"). - Description("Choose which service to use for text post-processing"). - Options(llmOptions...). - Value(&selectedProvider), - ), - ).WithTheme(getTheme()) - - if err := providerForm.Run(); err != nil { - return false, "", "", postProcessing, customPrompt, err - } - - modelOptions := getLLMModelOptions(selectedProvider) - var selectedModel string - if cfg.LLM.Model != "" { - selectedModel = cfg.LLM.Model - } else if len(modelOptions) > 0 { - selectedModel = modelOptions[0].Value - } - - modelForm := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("LLM Model"). - Options(modelOptions...). - Value(&selectedModel), - ), - ).WithTheme(getTheme()) - - if err := modelForm.Run(); err != nil { - return false, "", "", postProcessing, customPrompt, err - } - - if cfg.LLM.PostProcessing.RemoveStutters || cfg.LLM.PostProcessing.AddPunctuation || - cfg.LLM.PostProcessing.FixGrammar || cfg.LLM.PostProcessing.RemoveFillerWords { - postProcessing = cfg.LLM.PostProcessing - } - - var ppErr error - postProcessing, ppErr = selectPostProcessingOptions(postProcessing) - if ppErr != nil { - return false, "", "", postProcessing, customPrompt, ppErr - } - - var enableCustomPrompt bool - var customPromptText string - if cfg.LLM.CustomPrompt.Enabled { - enableCustomPrompt = true - customPromptText = cfg.LLM.CustomPrompt.Prompt - } - - customForm := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title("Add custom prompt?"). - Description("Add extra instructions for the LLM"). - Value(&enableCustomPrompt), - ), - ).WithTheme(getTheme()) - - if err := customForm.Run(); err != nil { - return false, "", "", postProcessing, customPrompt, err - } - - if enableCustomPrompt { - promptForm := huh.NewForm( - huh.NewGroup( - huh.NewText(). - Title("Custom Prompt"). - Description("Additional instructions (e.g., 'Format as bullet points')"). - Value(&customPromptText). - CharLimit(500), - ), - ).WithTheme(getTheme()) - - if err := promptForm.Run(); err != nil { - return false, "", "", postProcessing, customPrompt, err - } - customPrompt.Enabled = true - customPrompt.Prompt = customPromptText - } - - return true, selectedProvider, selectedModel, postProcessing, customPrompt, nil -} diff --git a/internal/tui/configure_providers.go b/internal/tui/configure_providers.go index 29beb3f..e0fe9a2 100644 --- a/internal/tui/configure_providers.go +++ b/internal/tui/configure_providers.go @@ -36,13 +36,17 @@ func getConfiguredProviders(cfg *config.Config) []string { } // editProviders handles the providers section edit with submenu -func editProviders(cfg *config.Config) error { +func editProviders(cfg *config.Config, onboarding bool) error { + exitLabel := "Done" + if onboarding { + exitLabel = "Next" + } for { var options []huh.Option[string] for _, name := range AllProviders { options = append(options, huh.NewOption(formatProviderOption(cfg, name), name)) } - options = append(options, huh.NewOption("Back", "back")) + options = append(options, huh.NewOption(exitLabel, "back")) var selected string form := huh.NewForm( diff --git a/internal/tui/configure_transcription.go b/internal/tui/configure_transcription.go index 2d1748e..dd99539 100644 --- a/internal/tui/configure_transcription.go +++ b/internal/tui/configure_transcription.go @@ -165,85 +165,3 @@ func getTranscriptionModelOptions(provider string) []huh.Option[string] { return []huh.Option[string]{} } } - -func configureTranscription(configuredProviders []string, cfg *config.Config) (string, string, string, error) { - var transcriptionOptions []huh.Option[string] - for _, name := range configuredProviders { - p := provider.GetProvider(name) - if p != nil && p.SupportsTranscription() { - switch name { - case "openai": - transcriptionOptions = append(transcriptionOptions, - huh.NewOption("OpenAI Whisper", "openai")) - case "groq": - transcriptionOptions = append(transcriptionOptions, - huh.NewOption("Groq Whisper (transcription)", "groq-transcription"), - huh.NewOption("Groq Whisper (translate to English)", "groq-translation")) - case "mistral": - transcriptionOptions = append(transcriptionOptions, - huh.NewOption("Mistral Voxtral", "mistral-transcription")) - case "elevenlabs": - transcriptionOptions = append(transcriptionOptions, - huh.NewOption("ElevenLabs Scribe", "elevenlabs")) - } - } - } - - if len(transcriptionOptions) == 0 { - return "", "", "", fmt.Errorf("no transcription-capable providers configured") - } - - var selectedProvider string - if cfg.Transcription.Provider != "" { - selectedProvider = cfg.Transcription.Provider - } else if len(transcriptionOptions) > 0 { - selectedProvider = transcriptionOptions[0].Value - } - - providerForm := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("Transcription Provider"). - Description("Choose which service to use for speech-to-text"). - Options(transcriptionOptions...). - Value(&selectedProvider), - ), - ).WithTheme(getTheme()) - - if err := providerForm.Run(); err != nil { - return "", "", "", err - } - - modelOptions := getTranscriptionModelOptions(selectedProvider) - var selectedModel string - if cfg.Transcription.Model != "" { - selectedModel = cfg.Transcription.Model - } else if len(modelOptions) > 0 { - selectedModel = modelOptions[0].Value - } - - var language string - if cfg.Transcription.Language != "" { - language = cfg.Transcription.Language - } - - modelForm := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("Transcription Model"). - Options(modelOptions...). - Value(&selectedModel), - huh.NewInput(). - Title("Language"). - Description("ISO-639-1 code (e.g., 'en', 'es', 'fr') or empty for auto-detect"). - Placeholder("auto-detect"). - Value(&language), - ), - ).WithTheme(getTheme()) - - if err := modelForm.Run(); err != nil { - return "", "", "", err - } - - return selectedProvider, selectedModel, language, nil -} diff --git a/internal/tui/configure_wizard.go b/internal/tui/configure_wizard.go index aab5925..e7817bc 100644 --- a/internal/tui/configure_wizard.go +++ b/internal/tui/configure_wizard.go @@ -8,111 +8,68 @@ import ( "github.com/leonardotrapani/hyprvoice/internal/config" ) -// runFreshInstall runs the full configuration wizard for fresh installs +// runFreshInstall runs the guided onboarding flow for fresh installs +// Uses the same screens as the menu for consistency func runFreshInstall(cfg *config.Config) (*ConfigureResult, error) { fmt.Println(Logo()) fmt.Println() fmt.Println(StyleMuted.Render("Voice-powered typing for Wayland/Hyprland")) fmt.Println() - selectedProviders, err := selectProviders() + // 1. Providers - same screen as menu + if err := editProviders(cfg, true); err != nil { + return &ConfigureResult{Cancelled: true}, nil + } + + configuredProviders := getConfiguredProviders(cfg) + if len(configuredProviders) == 0 { + return &ConfigureResult{Cancelled: true}, fmt.Errorf("no providers configured") + } + + // 2. Transcription - same screen as menu + var err error + configuredProviders, err = editTranscription(cfg, configuredProviders) if err != nil { return &ConfigureResult{Cancelled: true}, nil } - if len(selectedProviders) == 0 { - return &ConfigureResult{Cancelled: true}, fmt.Errorf("no providers selected") - } - if cfg.Providers == nil { - cfg.Providers = make(map[string]config.ProviderConfig) - } - - for _, providerName := range selectedProviders { - apiKey, err := inputAPIKey(providerName) - if err != nil { - return &ConfigureResult{Cancelled: true}, nil - } - cfg.Providers[providerName] = config.ProviderConfig{APIKey: apiKey} - } - - transcriptionProvider, transcriptionModel, language, err := configureTranscription(selectedProviders, cfg) + // 3. LLM - same screen as menu + configuredProviders, err = editLLM(cfg, configuredProviders) if err != nil { return &ConfigureResult{Cancelled: true}, nil } - cfg.Transcription.Provider = transcriptionProvider - cfg.Transcription.Model = transcriptionModel - cfg.Transcription.Language = language - - llmEnabled, llmProvider, llmModel, postProcessing, customPrompt, err := configureLLM(selectedProviders, cfg) - if err != nil { - return &ConfigureResult{Cancelled: true}, nil - } - cfg.LLM.Enabled = llmEnabled - cfg.LLM.Provider = llmProvider - cfg.LLM.Model = llmModel - cfg.LLM.PostProcessing = postProcessing - cfg.LLM.CustomPrompt = customPrompt + // 4. Keywords keywords, err := inputKeywords(cfg.Keywords) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Keywords = keywords + // 5. Injection backends backends, err := selectBackends(cfg.Injection.Backends) if err != nil { return &ConfigureResult{Cancelled: true}, nil } cfg.Injection.Backends = backends - notificationsEnabled, err := configureNotifications(cfg.Notifications.Enabled) + // 6. Notifications - same screen as menu + if err := editNotifications(cfg); err != nil { + return &ConfigureResult{Cancelled: true}, nil + } + + // 7. Advanced settings prompt + wantAdvanced, err := askAdvancedSettings() if err != nil { return &ConfigureResult{Cancelled: true}, nil } - cfg.Notifications.Enabled = notificationsEnabled - - confirmed, err := showSummary(cfg) - if err != nil || !confirmed { - return &ConfigureResult{Cancelled: true}, nil - } - - return &ConfigureResult{Config: cfg, Cancelled: false}, nil -} - -func selectProviders() ([]string, error) { - options := []huh.Option[string]{ - huh.NewOption("OpenAI - Whisper transcription + GPT for LLM", "openai"), - huh.NewOption("Groq - Fast Whisper transcription + Llama for LLM", "groq"), - huh.NewOption("Mistral - Voxtral transcription (European languages)", "mistral"), - huh.NewOption("ElevenLabs - Scribe transcription (99 languages)", "elevenlabs"), - } - - var selected []string - form := huh.NewForm( - huh.NewGroup( - huh.NewMultiSelect[string](). - Title("Which providers do you want to configure?"). - Description("Select all providers you have API keys for"). - Options(options...). - Value(&selected), - ), - ).WithTheme(getTheme()) - - if err := form.Run(); err != nil { - return nil, err - } - - valid := make([]string, 0) - for _, s := range selected { - for _, p := range AllProviders { - if s == p { - valid = append(valid, s) - break - } + if wantAdvanced { + if err := editAdvanced(cfg, true); err != nil { + return &ConfigureResult{Cancelled: true}, nil } } - return valid, nil + return &ConfigureResult{Config: cfg, Cancelled: false}, nil } func inputKeywords(existingKeywords []string) ([]string, error) { @@ -186,28 +143,19 @@ func selectBackends(existingBackends []string) ([]string, error) { return selected, nil } -func configureNotifications(existingEnabled bool) (bool, error) { - enabled := existingEnabled - - desc := "Show notifications for recording status changes" - if existingEnabled { - desc = "Currently: enabled. " + desc - } else { - desc = "Currently: disabled. " + desc - } - +func askAdvancedSettings() (bool, error) { + var want bool form := huh.NewForm( huh.NewGroup( huh.NewConfirm(). - Title("Enable desktop notifications?"). - Description(desc). - Value(&enabled), + Title("Configure advanced settings?"). + Description("Recording parameters, injection timeouts, etc."). + Value(&want), ), ).WithTheme(getTheme()) if err := form.Run(); err != nil { return false, err } - - return enabled, nil + return want, nil }