mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Fix/settings page (#4520)
This commit is contained in:
@@ -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.
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user