Fix/settings page (#4520)
This commit is contained in:
@@ -28,11 +28,8 @@ export default function ChatSettingsSection() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
<Card className="pb-2 rounded-lg">
|
<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">
|
<CardContent className="px-2">
|
||||||
<DictationSection />
|
<DictationSection />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,286 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { VoiceDictationToggle } from './VoiceDictationToggle';
|
||||||
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';
|
|
||||||
|
|
||||||
export default function DictationSection() {
|
export default function DictationSection() {
|
||||||
const [settings, setSettings] = useState<DictationSettings>({
|
return <VoiceDictationToggle />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useEffect, useState, useCallback } from 'react';
|
||||||
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
|
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
|
||||||
import { useConfig } from '../../ConfigContext';
|
import { useConfig } from '../../ConfigContext';
|
||||||
import { Input } from '../../ui/input';
|
import { ConversationLimitsDropdown } from './ConversationLimitsDropdown';
|
||||||
|
|
||||||
export const ModeSection = () => {
|
export const ModeSection = () => {
|
||||||
const [currentMode, setCurrentMode] = useState('auto');
|
const [currentMode, setCurrentMode] = useState('auto');
|
||||||
@@ -56,6 +56,7 @@ export const ModeSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
{/* Mode Selection */}
|
||||||
{all_goose_modes.map((mode) => (
|
{all_goose_modes.map((mode) => (
|
||||||
<ModeSelectionItem
|
<ModeSelectionItem
|
||||||
key={mode.key}
|
key={mode.key}
|
||||||
@@ -67,24 +68,11 @@ export const ModeSection = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="pt-6">
|
{/* Conversation Limits Dropdown */}
|
||||||
<h3 className="text-textStandard mb-4 pl-2">Conversation Limits</h3>
|
<ConversationLimitsDropdown
|
||||||
<div className="flex items-center justify-between py-2 px-4">
|
maxTurns={maxTurns}
|
||||||
<div>
|
onMaxTurnsChange={handleMaxTurnsChange}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ export function ResponseStyleSelectionItem({
|
|||||||
}, [currentStyle, style.key]);
|
}, [currentStyle, style.key]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group hover:cursor-pointer">
|
<div className="group hover:cursor-pointer text-sm">
|
||||||
<div
|
<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`}
|
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)}
|
onClick={() => handleStyleChange(style.key)}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-text-default text-xs">{style.label}</h3>
|
<h3 className="text-text-default">{style.label}</h3>
|
||||||
{showDescription && (
|
{showDescription && (
|
||||||
<p className="text-xs text-text-muted mt-[2px]">{style.description}</p>
|
<p className="text-xs text-text-muted mt-[2px]">{style.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { SchedulingEngine, Settings } from '../../../utils/settings';
|
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 {
|
interface SchedulerSectionProps {
|
||||||
onSchedulingEngineChange?: (engine: SchedulingEngine) => void;
|
onSchedulingEngineChange?: (engine: SchedulingEngine) => void;
|
||||||
}
|
}
|
||||||
@@ -9,7 +28,6 @@ export default function SchedulerSection({ onSchedulingEngineChange }: Scheduler
|
|||||||
const [schedulingEngine, setSchedulingEngine] = useState<SchedulingEngine>('builtin-cron');
|
const [schedulingEngine, setSchedulingEngine] = useState<SchedulingEngine>('builtin-cron');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load current scheduling engine setting
|
|
||||||
const loadSchedulingEngine = async () => {
|
const loadSchedulingEngine = async () => {
|
||||||
try {
|
try {
|
||||||
const settings = (await window.electron.getSettings()) as Settings | null;
|
const settings = (await window.electron.getSettings()) as Settings | null;
|
||||||
@@ -28,10 +46,8 @@ export default function SchedulerSection({ onSchedulingEngineChange }: Scheduler
|
|||||||
try {
|
try {
|
||||||
setSchedulingEngine(engine);
|
setSchedulingEngine(engine);
|
||||||
|
|
||||||
// Save the setting
|
|
||||||
await window.electron.setSchedulingEngine(engine);
|
await window.electron.setSchedulingEngine(engine);
|
||||||
|
|
||||||
// Notify parent component
|
|
||||||
if (onSchedulingEngineChange) {
|
if (onSchedulingEngineChange) {
|
||||||
onSchedulingEngineChange(engine);
|
onSchedulingEngineChange(engine);
|
||||||
}
|
}
|
||||||
@@ -41,52 +57,50 @@ export default function SchedulerSection({ onSchedulingEngineChange }: Scheduler
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4">
|
<div className="space-y-1">
|
||||||
<div className="space-y-3">
|
{schedulingEngineOptions.map((option) => {
|
||||||
<div className="flex items-start space-x-3">
|
const isChecked = schedulingEngine === option.key;
|
||||||
<input
|
|
||||||
type="radio"
|
return (
|
||||||
id="builtin-cron"
|
<div key={option.key} className="group hover:cursor-pointer text-sm">
|
||||||
name="schedulingEngine"
|
<div
|
||||||
value="builtin-cron"
|
className={`flex items-center justify-between text-text-default py-2 px-2 ${
|
||||||
checked={schedulingEngine === 'builtin-cron'}
|
isChecked
|
||||||
onChange={() => handleEngineChange('builtin-cron')}
|
? 'bg-background-muted'
|
||||||
className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
: 'bg-background-default hover:bg-background-muted'
|
||||||
/>
|
} rounded-lg transition-all`}
|
||||||
<div className="flex-1">
|
onClick={() => handleEngineChange(option.key)}
|
||||||
<label htmlFor="builtin-cron" className="block text-sm font-medium text-textStandard">
|
>
|
||||||
Built-in Cron (Default)
|
<div className="flex">
|
||||||
</label>
|
<div>
|
||||||
<p className="text-xs text-textSubtle mt-1">
|
<h3 className="text-text-default">{option.label}</h3>
|
||||||
Uses Goose's built-in cron scheduler. Simple and reliable for basic scheduling needs.
|
<p className="text-xs text-text-muted mt-[2px]">{option.description}</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
<div className="relative flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id="temporal"
|
name="schedulingEngine"
|
||||||
name="schedulingEngine"
|
value={option.key}
|
||||||
value="temporal"
|
checked={isChecked}
|
||||||
checked={schedulingEngine === 'temporal'}
|
onChange={() => handleEngineChange(option.key)}
|
||||||
onChange={() => handleEngineChange('temporal')}
|
className="peer sr-only"
|
||||||
className="mt-1 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
/>
|
||||||
/>
|
<div
|
||||||
<div className="flex-1">
|
className="h-4 w-4 rounded-full border border-border-default
|
||||||
<label htmlFor="temporal" className="block text-sm font-medium text-textStandard">
|
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||||
Temporal
|
peer-checked:bg-white dark:peer-checked:bg-black
|
||||||
</label>
|
transition-all duration-200 ease-in-out group-hover:border-border-default"
|
||||||
<p className="text-xs text-textSubtle mt-1">
|
></div>
|
||||||
Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI
|
</div>
|
||||||
to be installed.
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
})}
|
||||||
|
|
||||||
<div className="mt-4 p-3 bg-bgSubtle rounded-md">
|
<div className="mt-4 p-3 bg-background-subtle rounded-md">
|
||||||
<p className="text-xs text-textSubtle">
|
<p className="text-xs text-text-muted">
|
||||||
<strong>Note:</strong> Changing the scheduling engine will apply to new Goose sessions.
|
<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 />
|
You will need to restart Goose for the change to take full effect. <br />
|
||||||
The scheduling engines do not share the list of schedules.
|
The scheduling engines do not share the list of schedules.
|
||||||
|
|||||||
+2
-2
@@ -105,14 +105,14 @@ export const ToolSelectionStrategySection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{all_tool_selection_strategies.map((strategy) => (
|
{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
|
<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`}
|
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)}
|
onClick={() => handleStrategyChange(strategy.key)}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div>
|
<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>
|
<p className="text-xs text-text-muted mt-[2px]">{strategy.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user