Fix/settings page (#4520)

This commit is contained in:
Spence
2025-09-05 12:10:06 -04:00
committed by GitHub
parent 195f389972
commit 2448508691
11 changed files with 466 additions and 356 deletions
@@ -28,11 +28,8 @@ export default function ChatSettingsSection() {
</CardContent>
</Card>
<Card className="pb-2 rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="">Voice Dictation</CardTitle>
<CardDescription>Configure voice input for messages</CardDescription>
</CardHeader>
<CardContent className="px-2">
<DictationSection />
</CardContent>
@@ -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<DictationSettings>({
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 (
<section id="dictation" className="px-4">
{/* Enable/Disable Toggle */}
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-textStandard">Enable Voice Dictation</h3>
<p className="text-xs text-textSubtle max-w-md mt-[2px]">
Show microphone button for voice input
</p>
</div>
<div className="flex items-center">
<Switch checked={settings.enabled} onCheckedChange={handleToggle} variant="mono" />
</div>
</div>
{/* Provider Selection */}
{settings.enabled && (
<>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-textStandard">Dictation Provider</h3>
<p className="text-xs text-textSubtle max-w-md mt-[2px]">
Choose how voice is converted to text
</p>
</div>
<div className="relative">
<button
onClick={handleDropdownToggle}
className="flex items-center gap-2 px-3 py-1.5 text-sm border border-borderSubtle rounded-md hover:border-borderStandard transition-colors text-textStandard bg-background-default"
>
{getProviderLabel(settings.provider)}
<ChevronDown className="w-4 h-4" />
</button>
{showProviderDropdown && (
<div className="absolute right-0 mt-1 w-48 bg-background-default border border-borderStandard rounded-md shadow-lg z-10">
<button
onClick={() => handleProviderChange('openai')}
className="w-full px-3 py-2 text-left text-sm transition-colors first:rounded-t-md hover:bg-bgSubtle text-textStandard"
>
OpenAI Whisper
{!hasOpenAIKey && <span className="text-xs ml-1">(not configured)</span>}
{settings.provider === 'openai' && <span className="float-right"></span>}
</button>
{/* ElevenLabs option */}
<button
onClick={() => handleProviderChange('elevenlabs')}
className="w-full px-3 py-2 text-left text-sm hover:bg-bgSubtle transition-colors text-textStandard last:rounded-b-md"
>
ElevenLabs
{settings.provider === 'elevenlabs' && <span className="float-right"></span>}
</button>
</div>
)}
</div>
</div>
{/* ElevenLabs API Key */}
{showElevenLabsKey && (
<div className="mb-4">
<div className="mb-2">
<h3 className="text-textStandard">ElevenLabs API Key</h3>
<p className="text-xs text-textSubtle max-w-md mt-[2px]">
Required for ElevenLabs voice recognition
{hasElevenLabsKey && <span className="text-green-600 ml-2">(Configured)</span>}
</p>
</div>
<Input
type="password"
value={elevenLabsApiKey}
onChange={(e) => 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}
/>
</div>
)}
{/* Provider-specific information */}
<div className="mt-4 p-3 bg-bgSubtle rounded-md">
{settings.provider === 'openai' && (
<p className="text-xs text-textSubtle">
Uses OpenAI's Whisper API for high-quality transcription. Requires an OpenAI API key
configured in the Models section.
</p>
)}
{settings.provider === 'elevenlabs' && (
<div>
<p className="text-xs text-textSubtle">
Uses ElevenLabs speech-to-text API for high-quality transcription.
</p>
<p className="text-xs text-textSubtle mt-2">
<strong>Features:</strong>
</p>
<ul className="text-xs text-textSubtle ml-4 mt-1 list-disc">
<li>Advanced voice processing</li>
<li>High accuracy transcription</li>
<li>Multiple language support</li>
<li>Fast processing</li>
</ul>
<p className="text-xs text-textSubtle mt-2">
<strong>Note:</strong> Requires an ElevenLabs API key with speech-to-text access.
</p>
</div>
)}
</div>
</>
)}
</section>
);
return <VoiceDictationToggle />;
}
@@ -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 (
<div className="py-2 px-2 bg-background-subtle rounded-lg">
<div className="mb-2">
<h4 className="text-text-default text-sm">ElevenLabs API Key</h4>
<p className="text-xs text-text-muted mt-[2px]">
Required for ElevenLabs voice recognition
{hasElevenLabsKey && <span className="text-green-600 ml-2">(Configured)</span>}
</p>
</div>
<Input
type="password"
value={elevenLabsApiKey}
onChange={(e) => 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}
/>
</div>
);
};
@@ -0,0 +1,39 @@
import { DictationProvider } from '../../../hooks/useDictationSettings';
interface ProviderInfoProps {
provider: DictationProvider;
}
export const ProviderInfo = ({ provider }: ProviderInfoProps) => {
if (!provider) return null;
return (
<div className="p-3 bg-background-subtle rounded-md">
{provider === 'openai' && (
<p className="text-xs text-text-muted">
Uses OpenAI's Whisper API for high-quality transcription. Requires an OpenAI API key
configured in the Models section.
</p>
)}
{provider === 'elevenlabs' && (
<div>
<p className="text-xs text-text-muted">
Uses ElevenLabs speech-to-text API for high-quality transcription.
</p>
<p className="text-xs text-text-muted mt-2">
<strong>Features:</strong>
</p>
<ul className="text-xs text-text-muted ml-4 mt-1 list-disc">
<li>Advanced voice processing</li>
<li>High accuracy transcription</li>
<li>Multiple language support</li>
<li>Fast processing</li>
</ul>
<p className="text-xs text-text-muted mt-2">
<strong>Note:</strong> Requires an ElevenLabs API key with speech-to-text access.
</p>
</div>
)}
</div>
);
};
@@ -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 (
<div className="space-y-4">
<div className="flex items-center justify-between py-2 px-2 hover:bg-background-muted rounded-lg transition-all">
<div>
<h3 className="text-text-default">Dictation Provider</h3>
<p className="text-xs text-text-muted max-w-md mt-[2px]">
Choose how voice is converted to text
</p>
</div>
<div className="relative">
<button
onClick={handleDropdownToggle}
className="flex items-center gap-2 px-3 py-1.5 text-sm border border-border-subtle rounded-md hover:border-border-default transition-colors text-text-default bg-background-default"
>
{getProviderLabel(settings.provider)}
<ChevronDown className="w-4 h-4" />
</button>
{showProviderDropdown && (
<div className="absolute right-0 mt-1 w-48 bg-background-default border border-border-default rounded-md shadow-lg z-10">
<button
onClick={() => handleProviderChange('openai')}
className="w-full px-3 py-2 text-left text-sm transition-colors first:rounded-t-md hover:bg-background-subtle text-text-default"
>
OpenAI Whisper
{!hasOpenAIKey && <span className="text-xs ml-1">(not configured)</span>}
{settings.provider === 'openai' && <span className="float-right"></span>}
</button>
<button
onClick={() => handleProviderChange('elevenlabs')}
className="w-full px-3 py-2 text-left text-sm hover:bg-background-subtle transition-colors text-text-default last:rounded-b-md"
>
ElevenLabs
{settings.provider === 'elevenlabs' && <span className="float-right"></span>}
</button>
</div>
)}
</div>
</div>
{settings.provider === 'elevenlabs' && <ElevenLabsKeyInput />}
<ProviderInfo provider={settings.provider} />
</div>
);
};
@@ -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<DictationSettings>({
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 (
<div className="space-y-1">
<div className="flex items-center justify-between py-2 px-2 hover:bg-background-muted rounded-lg transition-all">
<div>
<h3 className="text-text-default">Enable Voice Dictation</h3>
<p className="text-xs text-text-muted max-w-md mt-[2px]">
Show microphone button for voice input
</p>
</div>
<div className="flex items-center">
<Switch checked={settings.enabled} onCheckedChange={handleToggle} variant="mono" />
</div>
</div>
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
settings.enabled
? 'max-h-96 opacity-100 mt-2'
: 'max-h-0 opacity-0 mt-0'
}`}
>
<div className="space-y-3 pb-2">
<ProviderSelector
settings={settings}
onProviderChange={handleProviderChange}
/>
</div>
</div>
</div>
);
};
@@ -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 (
<div className="pt-4">
<button
onClick={toggleExpanded}
className="w-full flex items-center justify-between py-2 px-2 hover:bg-background-muted rounded-lg transition-all group"
>
<h3 className="text-text-default font-medium">Conversation Limits</h3>
<ChevronDown
className={`w-4 h-4 text-text-muted transition-transform duration-200 ease-in-out ${
isExpanded ? 'rotate-180' : 'rotate-0'
}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${
isExpanded
? 'max-h-96 opacity-100 mt-2'
: 'max-h-0 opacity-0 mt-0'
}`}
>
<div className="space-y-3 pb-2">
<div className="flex items-center justify-between py-2 px-2 bg-background-subtle rounded-lg transform transition-all duration-200 ease-in-out">
<div>
<h4 className="text-text-default text-sm">Max Turns</h4>
<p className="text-xs text-text-muted mt-[2px]">
Maximum agent turns before Goose asks for user input
</p>
</div>
<Input
type="number"
min="1"
max="10000"
value={maxTurns}
onChange={(e) => onMaxTurnsChange(Number(e.target.value))}
className="w-20"
/>
</div>
</div>
</div>
</div>
);
};
@@ -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 (
<div className="space-y-1">
{/* Mode Selection */}
{all_goose_modes.map((mode) => (
<ModeSelectionItem
key={mode.key}
@@ -67,24 +68,11 @@ export const ModeSection = () => {
/>
))}
<div className="pt-6">
<h3 className="text-textStandard mb-4 pl-2">Conversation Limits</h3>
<div className="flex items-center justify-between py-2 px-4">
<div>
<h4 className="text-textStandard">Max Turns</h4>
<p className="text-xs text-textSubtle mt-[2px]">
Maximum agent turns before Goose asks for user input
</p>
</div>
<Input
type="number"
min="1"
value={maxTurns}
onChange={(e) => handleMaxTurnsChange(Number(e.target.value))}
className="w-20"
/>
</div>
</div>
{/* Conversation Limits Dropdown */}
<ConversationLimitsDropdown
maxTurns={maxTurns}
onMaxTurnsChange={handleMaxTurnsChange}
/>
</div>
);
};
@@ -39,14 +39,14 @@ export function ResponseStyleSelectionItem({
}, [currentStyle, style.key]);
return (
<div className="group hover:cursor-pointer">
<div className="group hover:cursor-pointer text-sm">
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${checked ? 'bg-background-muted' : 'bg-background-default hover:bg-background-muted'} rounded-lg transition-all`}
onClick={() => handleStyleChange(style.key)}
>
<div className="flex">
<div>
<h3 className="text-text-default text-xs">{style.label}</h3>
<h3 className="text-text-default">{style.label}</h3>
{showDescription && (
<p className="text-xs text-text-muted mt-[2px]">{style.description}</p>
)}
@@ -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<SchedulingEngine>('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 (
<div className="px-4">
<div className="space-y-3">
<div className="flex items-start space-x-3">
<input
type="radio"
id="builtin-cron"
name="schedulingEngine"
value="builtin-cron"
checked={schedulingEngine === 'builtin-cron'}
onChange={() => handleEngineChange('builtin-cron')}
className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<div className="flex-1">
<label htmlFor="builtin-cron" className="block text-sm font-medium text-textStandard">
Built-in Cron (Default)
</label>
<p className="text-xs text-textSubtle mt-1">
Uses Goose's built-in cron scheduler. Simple and reliable for basic scheduling needs.
</p>
</div>
</div>
<div className="space-y-1">
{schedulingEngineOptions.map((option) => {
const isChecked = schedulingEngine === option.key;
return (
<div key={option.key} className="group hover:cursor-pointer text-sm">
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${
isChecked
? 'bg-background-muted'
: 'bg-background-default hover:bg-background-muted'
} rounded-lg transition-all`}
onClick={() => handleEngineChange(option.key)}
>
<div className="flex">
<div>
<h3 className="text-text-default">{option.label}</h3>
<p className="text-xs text-text-muted mt-[2px]">{option.description}</p>
</div>
</div>
<div className="flex items-start space-x-3">
<input
type="radio"
id="temporal"
name="schedulingEngine"
value="temporal"
checked={schedulingEngine === 'temporal'}
onChange={() => handleEngineChange('temporal')}
className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<div className="flex-1">
<label htmlFor="temporal" className="block text-sm font-medium text-textStandard">
Temporal
</label>
<p className="text-xs text-textSubtle mt-1">
Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI
to be installed.
</p>
<div className="relative flex items-center gap-2">
<input
type="radio"
name="schedulingEngine"
value={option.key}
checked={isChecked}
onChange={() => handleEngineChange(option.key)}
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-border-default
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
peer-checked:bg-white dark:peer-checked:bg-black
transition-all duration-200 ease-in-out group-hover:border-border-default"
></div>
</div>
</div>
</div>
</div>
</div>
);
})}
<div className="mt-4 p-3 bg-bgSubtle rounded-md">
<p className="text-xs text-textSubtle">
<div className="mt-4 p-3 bg-background-subtle rounded-md">
<p className="text-xs text-text-muted">
<strong>Note:</strong> Changing the scheduling engine will apply to new Goose sessions.
You will need to restart Goose for the change to take full effect. <br />
The scheduling engines do not share the list of schedules.
@@ -105,14 +105,14 @@ export const ToolSelectionStrategySection = () => {
return (
<div className="space-y-1">
{all_tool_selection_strategies.map((strategy) => (
<div className="group hover:cursor-pointer" key={strategy.key.toString()}>
<div className="group hover:cursor-pointer text-sm" key={strategy.key.toString()}>
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${routerEnabled === strategy.key ? 'bg-background-muted' : 'bg-background-default hover:bg-background-muted'} rounded-lg transition-all`}
onClick={() => handleStrategyChange(strategy.key)}
>
<div className="flex">
<div>
<h3 className="text-text-default text-xs">{strategy.label}</h3>
<h3 className="text-text-default">{strategy.label}</h3>
<p className="text-xs text-text-muted mt-[2px]">{strategy.description}</p>
</div>
</div>