diff --git a/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx b/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx index d7661bebda..60888c1978 100644 --- a/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx +++ b/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx @@ -28,11 +28,8 @@ export default function ChatSettingsSection() { + - - Voice Dictation - Configure voice input for messages - diff --git a/ui/desktop/src/components/settings/dictation/DictationSection.tsx b/ui/desktop/src/components/settings/dictation/DictationSection.tsx index f28baa79f3..a4abcfcc08 100644 --- a/ui/desktop/src/components/settings/dictation/DictationSection.tsx +++ b/ui/desktop/src/components/settings/dictation/DictationSection.tsx @@ -1,286 +1,5 @@ -import { useState, useEffect, useRef } from 'react'; -import { Switch } from '../../ui/switch'; -import { ChevronDown } from 'lucide-react'; -import { Input } from '../../ui/input'; -import { useConfig } from '../../ConfigContext'; -import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; -import { - DICTATION_SETTINGS_KEY, - ELEVENLABS_API_KEY, - getDefaultDictationSettings, -} from '../../../hooks/dictationConstants'; +import { VoiceDictationToggle } from './VoiceDictationToggle'; export default function DictationSection() { - const [settings, setSettings] = useState({ - enabled: false, - provider: null, - }); - const [hasOpenAIKey, setHasOpenAIKey] = useState(false); - const [showProviderDropdown, setShowProviderDropdown] = useState(false); - const [showElevenLabsKey, setShowElevenLabsKey] = useState(false); - const [elevenLabsApiKey, setElevenLabsApiKey] = useState(''); - const [isLoadingKey, setIsLoadingKey] = useState(false); - const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false); - const elevenLabsApiKeyRef = useRef(''); - - const { getProviders, upsert, read } = useConfig(); - - // Load settings from localStorage and ElevenLabs API key from secure storage - useEffect(() => { - const loadSettings = async () => { - const savedSettings = localStorage.getItem(DICTATION_SETTINGS_KEY); - - let loadedSettings: DictationSettings; - - if (savedSettings) { - const parsed = JSON.parse(savedSettings); - loadedSettings = parsed; - } else { - loadedSettings = await getDefaultDictationSettings(getProviders); - } - - setSettings(loadedSettings); - setShowElevenLabsKey(loadedSettings.provider === 'elevenlabs'); - - // Load ElevenLabs API key from storage - setIsLoadingKey(true); - try { - // Try reading as secret - will return true if exists - const keyExists = await read(ELEVENLABS_API_KEY, true); - if (keyExists === true) { - setHasElevenLabsKey(true); - // Don't set the actual key since we can't read secrets - } - } catch (error) { - console.error('Error checking ElevenLabs API key:', error); - } finally { - setIsLoadingKey(false); - } - }; - - loadSettings(); - }, [read, getProviders]); - - // Save ElevenLabs key on unmount if it has changed - useEffect(() => { - return () => { - if (showElevenLabsKey && elevenLabsApiKeyRef.current) { - // We can't use async in cleanup, so we'll use the promise directly - const keyToSave = elevenLabsApiKeyRef.current; - if (keyToSave.trim()) { - upsert(ELEVENLABS_API_KEY, keyToSave, true).catch((error) => { - console.error('Error saving ElevenLabs API key on unmount:', error); - }); - } - } - }; - }, [showElevenLabsKey, upsert]); - - // Check if OpenAI is configured - useEffect(() => { - const checkOpenAIKey = async () => { - try { - const providers = await getProviders(false); - const openAIProvider = providers.find((p) => p.name === 'openai'); - setHasOpenAIKey(openAIProvider?.is_configured || false); - } catch (error) { - console.error('Error checking OpenAI configuration:', error); - setHasOpenAIKey(false); - } - }; - - checkOpenAIKey(); - }, [getProviders]); - - const handleDropdownToggle = async () => { - const newShowState = !showProviderDropdown; - setShowProviderDropdown(newShowState); - - if (newShowState) { - try { - const providers = await getProviders(true); - const openAIProvider = providers.find((p) => p.name === 'openai'); - const isConfigured = !!openAIProvider?.is_configured; - setHasOpenAIKey(isConfigured); - } catch (error) { - console.error('Error checking OpenAI configuration:', error); - setHasOpenAIKey(false); - } - } - }; - - const saveSettings = (newSettings: DictationSettings) => { - console.log('Saving dictation settings to localStorage:', newSettings); - setSettings(newSettings); - localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(newSettings)); - }; - - const handleToggle = (enabled: boolean) => { - saveSettings({ - ...settings, - enabled, - provider: settings.provider === null ? 'openai' : settings.provider, - }); - }; - - const handleProviderChange = (provider: DictationProvider) => { - saveSettings({ ...settings, provider }); - setShowProviderDropdown(false); - setShowElevenLabsKey(provider === 'elevenlabs'); - }; - - const handleElevenLabsKeyChange = (key: string) => { - setElevenLabsApiKey(key); - elevenLabsApiKeyRef.current = key; - // If user starts typing, they're updating the key - if (key.length > 0) { - setHasElevenLabsKey(false); // Hide "configured" while typing - } - }; - - const saveElevenLabsKey = async () => { - // Save to secure storage - try { - if (elevenLabsApiKey.trim()) { - console.log('Saving ElevenLabs API key to secure storage...'); - await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true); - setHasElevenLabsKey(true); - console.log('ElevenLabs API key saved successfully'); - } else { - // If key is empty, remove it from storage - console.log('Removing ElevenLabs API key from secure storage...'); - await upsert(ELEVENLABS_API_KEY, null, true); - setHasElevenLabsKey(false); - console.log('ElevenLabs API key removed successfully'); - } - } catch (error) { - console.error('Error saving ElevenLabs API key:', error); - } - }; - - const getProviderLabel = (provider: DictationProvider): string => { - switch (provider) { - case 'openai': - return 'OpenAI Whisper'; - case 'elevenlabs': - return 'ElevenLabs'; - default: - return 'None (disabled)'; - } - }; - - return ( -
- {/* Enable/Disable Toggle */} -
-
-

Enable Voice Dictation

-

- Show microphone button for voice input -

-
-
- -
-
- - {/* Provider Selection */} - {settings.enabled && ( - <> -
-
-

Dictation Provider

-

- Choose how voice is converted to text -

-
-
- - - {showProviderDropdown && ( -
- - - {/* ElevenLabs option */} - -
- )} -
-
- - {/* ElevenLabs API Key */} - {showElevenLabsKey && ( -
-
-

ElevenLabs API Key

-

- Required for ElevenLabs voice recognition - {hasElevenLabsKey && (Configured)} -

-
- handleElevenLabsKeyChange(e.target.value)} - onBlur={saveElevenLabsKey} - placeholder={ - hasElevenLabsKey ? 'Enter new API key to update' : 'Enter your ElevenLabs API key' - } - className="max-w-md" - disabled={isLoadingKey} - /> -
- )} - - {/* Provider-specific information */} -
- {settings.provider === 'openai' && ( -

- Uses OpenAI's Whisper API for high-quality transcription. Requires an OpenAI API key - configured in the Models section. -

- )} - {settings.provider === 'elevenlabs' && ( -
-

- Uses ElevenLabs speech-to-text API for high-quality transcription. -

-

- Features: -

-
    -
  • Advanced voice processing
  • -
  • High accuracy transcription
  • -
  • Multiple language support
  • -
  • Fast processing
  • -
-

- Note: Requires an ElevenLabs API key with speech-to-text access. -

-
- )} -
- - )} -
- ); + return ; } diff --git a/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx b/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx new file mode 100644 index 0000000000..4cce6d21f6 --- /dev/null +++ b/ui/desktop/src/components/settings/dictation/ElevenLabsKeyInput.tsx @@ -0,0 +1,93 @@ +import { useState, useEffect, useRef } from 'react'; +import { Input } from '../../ui/input'; +import { useConfig } from '../../ConfigContext'; +import { ELEVENLABS_API_KEY } from '../../../hooks/dictationConstants'; + +export const ElevenLabsKeyInput = () => { + const [elevenLabsApiKey, setElevenLabsApiKey] = useState(''); + const [isLoadingKey, setIsLoadingKey] = useState(false); + const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false); + const elevenLabsApiKeyRef = useRef(''); + const { upsert, read } = useConfig(); + + useEffect(() => { + const loadKey = async () => { + setIsLoadingKey(true); + try { + const keyExists = await read(ELEVENLABS_API_KEY, true); + if (keyExists === true) { + setHasElevenLabsKey(true); + } + } catch (error) { + console.error('Error checking ElevenLabs API key:', error); + } finally { + setIsLoadingKey(false); + } + }; + + loadKey(); + }, [read]); + + // Save key on unmount to avoid losing unsaved changes + useEffect(() => { + return () => { + if (elevenLabsApiKeyRef.current) { + const keyToSave = elevenLabsApiKeyRef.current; + if (keyToSave.trim()) { + upsert(ELEVENLABS_API_KEY, keyToSave, true).catch((error) => { + console.error('Error saving ElevenLabs API key on unmount:', error); + }); + } + } + }; + }, [upsert]); + + const handleElevenLabsKeyChange = (key: string) => { + setElevenLabsApiKey(key); + elevenLabsApiKeyRef.current = key; + if (key.length > 0) { + setHasElevenLabsKey(false); + } + }; + + const saveElevenLabsKey = async () => { + try { + if (elevenLabsApiKey.trim()) { + console.log('Saving ElevenLabs API key to secure storage...'); + await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true); + setHasElevenLabsKey(true); + console.log('ElevenLabs API key saved successfully'); + } else { + console.log('Removing ElevenLabs API key from secure storage...'); + await upsert(ELEVENLABS_API_KEY, null, true); + setHasElevenLabsKey(false); + console.log('ElevenLabs API key removed successfully'); + } + } catch (error) { + console.error('Error saving ElevenLabs API key:', error); + } + }; + + return ( +
+
+

ElevenLabs API Key

+

+ Required for ElevenLabs voice recognition + {hasElevenLabsKey && (Configured)} +

+
+ handleElevenLabsKeyChange(e.target.value)} + onBlur={saveElevenLabsKey} + placeholder={ + hasElevenLabsKey ? 'Enter new API key to update' : 'Enter your ElevenLabs API key' + } + className="max-w-md" + disabled={isLoadingKey} + /> +
+ ); +}; diff --git a/ui/desktop/src/components/settings/dictation/ProviderInfo.tsx b/ui/desktop/src/components/settings/dictation/ProviderInfo.tsx new file mode 100644 index 0000000000..2cfedd7f1d --- /dev/null +++ b/ui/desktop/src/components/settings/dictation/ProviderInfo.tsx @@ -0,0 +1,39 @@ +import { DictationProvider } from '../../../hooks/useDictationSettings'; + +interface ProviderInfoProps { + provider: DictationProvider; +} + +export const ProviderInfo = ({ provider }: ProviderInfoProps) => { + if (!provider) return null; + + return ( +
+ {provider === 'openai' && ( +

+ Uses OpenAI's Whisper API for high-quality transcription. Requires an OpenAI API key + configured in the Models section. +

+ )} + {provider === 'elevenlabs' && ( +
+

+ Uses ElevenLabs speech-to-text API for high-quality transcription. +

+

+ Features: +

+
    +
  • Advanced voice processing
  • +
  • High accuracy transcription
  • +
  • Multiple language support
  • +
  • Fast processing
  • +
+

+ Note: Requires an ElevenLabs API key with speech-to-text access. +

+
+ )} +
+ ); +}; diff --git a/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx b/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx new file mode 100644 index 0000000000..b1b868b6af --- /dev/null +++ b/ui/desktop/src/components/settings/dictation/ProviderSelector.tsx @@ -0,0 +1,112 @@ +import { useState, useEffect } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; +import { useConfig } from '../../ConfigContext'; +import { ElevenLabsKeyInput } from './ElevenLabsKeyInput'; +import { ProviderInfo } from './ProviderInfo'; + +interface ProviderSelectorProps { + settings: DictationSettings; + onProviderChange: (provider: DictationProvider) => void; +} + +export const ProviderSelector = ({ settings, onProviderChange }: ProviderSelectorProps) => { + const [hasOpenAIKey, setHasOpenAIKey] = useState(false); + const [showProviderDropdown, setShowProviderDropdown] = useState(false); + const { getProviders } = useConfig(); + + useEffect(() => { + const checkOpenAIKey = async () => { + try { + const providers = await getProviders(false); + const openAIProvider = providers.find((p) => p.name === 'openai'); + setHasOpenAIKey(openAIProvider?.is_configured || false); + } catch (error) { + console.error('Error checking OpenAI configuration:', error); + setHasOpenAIKey(false); + } + }; + + checkOpenAIKey(); + }, [getProviders]); + + const handleDropdownToggle = async () => { + const newShowState = !showProviderDropdown; + setShowProviderDropdown(newShowState); + + if (newShowState) { + try { + const providers = await getProviders(true); + const openAIProvider = providers.find((p) => p.name === 'openai'); + const isConfigured = !!openAIProvider?.is_configured; + setHasOpenAIKey(isConfigured); + } catch (error) { + console.error('Error checking OpenAI configuration:', error); + setHasOpenAIKey(false); + } + } + }; + + const handleProviderChange = (provider: DictationProvider) => { + onProviderChange(provider); + setShowProviderDropdown(false); + }; + + const getProviderLabel = (provider: DictationProvider): string => { + switch (provider) { + case 'openai': + return 'OpenAI Whisper'; + case 'elevenlabs': + return 'ElevenLabs'; + default: + return 'None (disabled)'; + } + }; + + return ( +
+
+
+

Dictation Provider

+

+ Choose how voice is converted to text +

+
+
+ + + {showProviderDropdown && ( +
+ + + +
+ )} +
+
+ + {settings.provider === 'elevenlabs' && } + + +
+ ); +}; diff --git a/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx b/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx new file mode 100644 index 0000000000..14448e9e0e --- /dev/null +++ b/ui/desktop/src/components/settings/dictation/VoiceDictationToggle.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react'; +import { Switch } from '../../ui/switch'; +import { DictationProvider, DictationSettings } from '../../../hooks/useDictationSettings'; +import { + DICTATION_SETTINGS_KEY, + getDefaultDictationSettings, +} from '../../../hooks/dictationConstants'; +import { useConfig } from '../../ConfigContext'; +import { ProviderSelector } from './ProviderSelector'; + +export const VoiceDictationToggle = () => { + const [settings, setSettings] = useState({ + enabled: false, + provider: null, + }); + const { getProviders } = useConfig(); + + useEffect(() => { + const loadSettings = async () => { + const savedSettings = localStorage.getItem(DICTATION_SETTINGS_KEY); + + let loadedSettings: DictationSettings; + + if (savedSettings) { + const parsed = JSON.parse(savedSettings); + loadedSettings = parsed; + } else { + loadedSettings = await getDefaultDictationSettings(getProviders); + } + + setSettings(loadedSettings); + }; + + loadSettings(); + }, [getProviders]); + + const saveSettings = (newSettings: DictationSettings) => { + console.log('Saving dictation settings to localStorage:', newSettings); + setSettings(newSettings); + localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(newSettings)); + }; + + const handleToggle = (enabled: boolean) => { + saveSettings({ + ...settings, + enabled, + provider: settings.provider === null ? 'openai' : settings.provider, + }); + }; + + const handleProviderChange = (provider: DictationProvider) => { + saveSettings({ ...settings, provider }); + }; + + return ( +
+
+
+

Enable Voice Dictation

+

+ Show microphone button for voice input +

+
+
+ +
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx b/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx new file mode 100644 index 0000000000..f4ade89cf8 --- /dev/null +++ b/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { ChevronDown } from 'lucide-react'; +import { Input } from '../../ui/input'; + +interface ConversationLimitsDropdownProps { + maxTurns: number; + onMaxTurnsChange: (value: number) => void; +} + +export const ConversationLimitsDropdown = ({ + maxTurns, + onMaxTurnsChange +}: ConversationLimitsDropdownProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + return ( +
+ + +
+
+
+
+

Max Turns

+

+ Maximum agent turns before Goose asks for user input +

+
+ onMaxTurnsChange(Number(e.target.value))} + className="w-20" + /> +
+
+
+
+ ); +}; diff --git a/ui/desktop/src/components/settings/mode/ModeSection.tsx b/ui/desktop/src/components/settings/mode/ModeSection.tsx index a53b86272b..9b30976312 100644 --- a/ui/desktop/src/components/settings/mode/ModeSection.tsx +++ b/ui/desktop/src/components/settings/mode/ModeSection.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useCallback } from 'react'; import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem'; import { useConfig } from '../../ConfigContext'; -import { Input } from '../../ui/input'; +import { ConversationLimitsDropdown } from './ConversationLimitsDropdown'; export const ModeSection = () => { const [currentMode, setCurrentMode] = useState('auto'); @@ -56,6 +56,7 @@ export const ModeSection = () => { return (
+ {/* Mode Selection */} {all_goose_modes.map((mode) => ( { /> ))} -
-

Conversation Limits

-
-
-

Max Turns

-

- Maximum agent turns before Goose asks for user input -

-
- handleMaxTurnsChange(Number(e.target.value))} - className="w-20" - /> -
-
+ {/* Conversation Limits Dropdown */} +
); }; diff --git a/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx b/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx index 04c4b3e96c..f42bc40661 100644 --- a/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx +++ b/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx @@ -39,14 +39,14 @@ export function ResponseStyleSelectionItem({ }, [currentStyle, style.key]); return ( -
+
handleStyleChange(style.key)} >
-

{style.label}

+

{style.label}

{showDescription && (

{style.description}

)} diff --git a/ui/desktop/src/components/settings/scheduler/SchedulerSection.tsx b/ui/desktop/src/components/settings/scheduler/SchedulerSection.tsx index 2e54764b3b..1999011d06 100644 --- a/ui/desktop/src/components/settings/scheduler/SchedulerSection.tsx +++ b/ui/desktop/src/components/settings/scheduler/SchedulerSection.tsx @@ -1,6 +1,25 @@ import { useState, useEffect } from 'react'; import { SchedulingEngine, Settings } from '../../../utils/settings'; +interface SchedulingEngineOption { + key: SchedulingEngine; + label: string; + description: string; +} + +const schedulingEngineOptions: SchedulingEngineOption[] = [ + { + key: 'builtin-cron', + label: 'Built-in Cron (Default)', + description: 'Uses Goose\'s built-in cron scheduler. Simple and reliable for basic scheduling needs.', + }, + { + key: 'temporal', + label: 'Temporal', + description: 'Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI to be installed.', + }, +]; + interface SchedulerSectionProps { onSchedulingEngineChange?: (engine: SchedulingEngine) => void; } @@ -9,7 +28,6 @@ export default function SchedulerSection({ onSchedulingEngineChange }: Scheduler const [schedulingEngine, setSchedulingEngine] = useState('builtin-cron'); useEffect(() => { - // Load current scheduling engine setting const loadSchedulingEngine = async () => { try { const settings = (await window.electron.getSettings()) as Settings | null; @@ -28,10 +46,8 @@ export default function SchedulerSection({ onSchedulingEngineChange }: Scheduler try { setSchedulingEngine(engine); - // Save the setting await window.electron.setSchedulingEngine(engine); - // Notify parent component if (onSchedulingEngineChange) { onSchedulingEngineChange(engine); } @@ -41,52 +57,50 @@ export default function SchedulerSection({ onSchedulingEngineChange }: Scheduler }; return ( -
-
-
- handleEngineChange('builtin-cron')} - className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" - /> -
- -

- Uses Goose's built-in cron scheduler. Simple and reliable for basic scheduling needs. -

-
-
+
+ {schedulingEngineOptions.map((option) => { + const isChecked = schedulingEngine === option.key; + + return ( +
+
handleEngineChange(option.key)} + > +
+
+

{option.label}

+

{option.description}

+
+
-
- handleEngineChange('temporal')} - className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" - /> -
- -

- Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI - to be installed. -

+
+ handleEngineChange(option.key)} + className="peer sr-only" + /> +
+
+
-
-
+ ); + })} -
-

+

+

Note: Changing the scheduling engine will apply to new Goose sessions. You will need to restart Goose for the change to take full effect.
The scheduling engines do not share the list of schedules. diff --git a/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx b/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx index 3d40b547bb..1aeceb5a65 100644 --- a/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx +++ b/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx @@ -105,14 +105,14 @@ export const ToolSelectionStrategySection = () => { return (

{all_tool_selection_strategies.map((strategy) => ( -
+
handleStrategyChange(strategy.key)} >
-

{strategy.label}

+

{strategy.label}

{strategy.description}