|
|
|
@@ -6,10 +6,17 @@ import { Input } from '../../ui/input';
|
|
|
|
|
import { Button } from '../../ui/button';
|
|
|
|
|
import { trackSettingToggled } from '../../../utils/analytics';
|
|
|
|
|
import { LocalModelManager } from './LocalModelManager';
|
|
|
|
|
import { DICTATION_ALLOWED_PROVIDERS } from '../../../updates';
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuRadioGroup,
|
|
|
|
|
DropdownMenuRadioItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from '../../ui/dropdown-menu';
|
|
|
|
|
|
|
|
|
|
export const DictationSettings = () => {
|
|
|
|
|
const [provider, setProvider] = useState<DictationProvider | null>(null);
|
|
|
|
|
const [showProviderDropdown, setShowProviderDropdown] = useState(false);
|
|
|
|
|
const [providerStatuses, setProviderStatuses] = useState<Record<string, DictationProviderStatus>>(
|
|
|
|
|
{}
|
|
|
|
|
);
|
|
|
|
@@ -17,41 +24,39 @@ export const DictationSettings = () => {
|
|
|
|
|
const [isEditingKey, setIsEditingKey] = useState(false);
|
|
|
|
|
const { read, upsert, remove } = useConfig();
|
|
|
|
|
|
|
|
|
|
const refreshStatuses = async () => {
|
|
|
|
|
const audioConfig = await getDictationConfig();
|
|
|
|
|
setProviderStatuses(audioConfig.data || {});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadSettings = async () => {
|
|
|
|
|
const providerValue = await read('voice_dictation_provider', false);
|
|
|
|
|
const loadedProvider: DictationProvider | null = (providerValue as DictationProvider) || null;
|
|
|
|
|
setProvider(loadedProvider);
|
|
|
|
|
let loadedProvider: DictationProvider | null = (providerValue as DictationProvider) || null;
|
|
|
|
|
|
|
|
|
|
const audioConfig = await getDictationConfig();
|
|
|
|
|
setProviderStatuses(audioConfig.data || {});
|
|
|
|
|
if (
|
|
|
|
|
DICTATION_ALLOWED_PROVIDERS &&
|
|
|
|
|
loadedProvider &&
|
|
|
|
|
!DICTATION_ALLOWED_PROVIDERS.includes(loadedProvider)
|
|
|
|
|
) {
|
|
|
|
|
loadedProvider = null;
|
|
|
|
|
await upsert('voice_dictation_provider', '', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setProvider(loadedProvider);
|
|
|
|
|
await refreshStatuses();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadSettings();
|
|
|
|
|
}, [read]);
|
|
|
|
|
}, [read, upsert]);
|
|
|
|
|
|
|
|
|
|
const saveProvider = async (newProvider: DictationProvider | null) => {
|
|
|
|
|
console.log('Saving dictation provider to backend config:', newProvider);
|
|
|
|
|
const handleProviderChange = (value: string) => {
|
|
|
|
|
const newProvider = value === 'disabled' ? null : (value as DictationProvider);
|
|
|
|
|
setProvider(newProvider);
|
|
|
|
|
await upsert('voice_dictation_provider', newProvider || '', false);
|
|
|
|
|
upsert('voice_dictation_provider', newProvider || '', false);
|
|
|
|
|
trackSettingToggled('voice_dictation', newProvider !== null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleProviderChange = (newProvider: DictationProvider | null) => {
|
|
|
|
|
saveProvider(newProvider);
|
|
|
|
|
setShowProviderDropdown(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDropdownToggle = async () => {
|
|
|
|
|
const newShowState = !showProviderDropdown;
|
|
|
|
|
setShowProviderDropdown(newShowState);
|
|
|
|
|
|
|
|
|
|
if (newShowState) {
|
|
|
|
|
const audioConfig = await getDictationConfig();
|
|
|
|
|
setProviderStatuses(audioConfig.data || {});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSaveKey = async () => {
|
|
|
|
|
if (!provider) return;
|
|
|
|
|
const providerConfig = providerStatuses[provider];
|
|
|
|
@@ -64,9 +69,7 @@ export const DictationSettings = () => {
|
|
|
|
|
await upsert(keyName, trimmedKey, true);
|
|
|
|
|
setApiKey('');
|
|
|
|
|
setIsEditingKey(false);
|
|
|
|
|
|
|
|
|
|
const audioConfig = await getDictationConfig();
|
|
|
|
|
setProviderStatuses(audioConfig.data || {});
|
|
|
|
|
await refreshStatuses();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRemoveKey = async () => {
|
|
|
|
@@ -78,9 +81,7 @@ export const DictationSettings = () => {
|
|
|
|
|
await remove(keyName, true);
|
|
|
|
|
setApiKey('');
|
|
|
|
|
setIsEditingKey(false);
|
|
|
|
|
|
|
|
|
|
const audioConfig = await getDictationConfig();
|
|
|
|
|
setProviderStatuses(audioConfig.data || {});
|
|
|
|
|
await refreshStatuses();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancelEdit = () => {
|
|
|
|
@@ -88,11 +89,15 @@ export const DictationSettings = () => {
|
|
|
|
|
setIsEditingKey(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getProviderLabel = (provider: DictationProvider | null): string => {
|
|
|
|
|
if (!provider) return 'Disabled';
|
|
|
|
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
|
|
|
const getProviderLabel = (p: DictationProvider | null): string => {
|
|
|
|
|
if (!p) return 'Disabled';
|
|
|
|
|
return p.charAt(0).toUpperCase() + p.slice(1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const visibleProviders = (Object.keys(providerStatuses) as DictationProvider[]).filter(
|
|
|
|
|
(p) => !DICTATION_ALLOWED_PROVIDERS || DICTATION_ALLOWED_PROVIDERS.includes(p)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between py-2 px-2 hover:bg-background-secondary rounded-lg transition-all">
|
|
|
|
@@ -102,47 +107,28 @@ export const DictationSettings = () => {
|
|
|
|
|
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-primary rounded-md hover:border-border-primary transition-colors text-text-primary bg-background-primary"
|
|
|
|
|
>
|
|
|
|
|
<DropdownMenu onOpenChange={(open) => open && refreshStatuses()}>
|
|
|
|
|
<DropdownMenuTrigger className="flex items-center gap-2 px-3 py-1.5 text-sm border border-border-primary rounded-md hover:border-border-primary transition-colors text-text-primary bg-background-primary">
|
|
|
|
|
{getProviderLabel(provider)}
|
|
|
|
|
<ChevronDown className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{showProviderDropdown && (
|
|
|
|
|
<div className="absolute right-0 mt-1 w-max min-w-[250px] max-w-[350px] bg-background-primary border border-border-primary rounded-md shadow-lg z-50">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleProviderChange(null)}
|
|
|
|
|
className="w-full px-3 py-2 text-left text-sm transition-colors hover:bg-background-secondary text-text-primary whitespace-nowrap first:rounded-t-md"
|
|
|
|
|
>
|
|
|
|
|
<span className="flex items-center justify-between gap-2">
|
|
|
|
|
<span>Disabled</span>
|
|
|
|
|
{provider === null && <span>✓</span>}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{(Object.keys(providerStatuses) as DictationProvider[]).map((p) => (
|
|
|
|
|
<button
|
|
|
|
|
key={p}
|
|
|
|
|
onClick={() => handleProviderChange(p)}
|
|
|
|
|
className="w-full px-3 py-2 text-left text-sm transition-colors hover:bg-background-secondary text-text-primary whitespace-nowrap last:rounded-b-md"
|
|
|
|
|
>
|
|
|
|
|
<span className="flex items-center justify-between gap-2">
|
|
|
|
|
<span>
|
|
|
|
|
{getProviderLabel(p)}
|
|
|
|
|
{!providerStatuses[p]?.configured && (
|
|
|
|
|
<span className="text-xs ml-1 text-text-secondary">(not configured)</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
{provider === p && <span>✓</span>}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="end" className="w-max min-w-[250px] max-w-[350px]">
|
|
|
|
|
<DropdownMenuRadioGroup
|
|
|
|
|
value={provider ?? 'disabled'}
|
|
|
|
|
onValueChange={handleProviderChange}
|
|
|
|
|
>
|
|
|
|
|
<DropdownMenuRadioItem value="disabled">Disabled</DropdownMenuRadioItem>
|
|
|
|
|
{visibleProviders.map((p) => (
|
|
|
|
|
<DropdownMenuRadioItem key={p} value={p}>
|
|
|
|
|
{getProviderLabel(p)}
|
|
|
|
|
{!providerStatuses[p]?.configured && (
|
|
|
|
|
<span className="text-xs ml-1 text-text-secondary">(not configured)</span>
|
|
|
|
|
)}
|
|
|
|
|
</DropdownMenuRadioItem>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</DropdownMenuRadioGroup>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{provider && providerStatuses[provider] && (
|
|
|
|
|