Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions ui/desktop/src/components/Layout/NavigationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,12 @@ export const NavigationProvider: React.FC<NavigationProviderProps> = ({ children
}, [setIsNavExpanded]);

useEffect(() => {
const handleModeChange = (e: Event) =>
setNavigationModeState((e as CustomEvent).detail.mode);
const handleModeChange = (e: Event) => setNavigationModeState((e as CustomEvent).detail.mode);
const handleStyleChange = (e: Event) =>
setNavigationStyleState((e as CustomEvent).detail.style);
const handlePositionChange = (e: Event) =>
setNavigationPositionState((e as CustomEvent).detail.position);
const handlePrefsChange = (e: Event) =>
setPreferences((e as CustomEvent).detail);
const handlePrefsChange = (e: Event) => setPreferences((e as CustomEvent).detail);

window.addEventListener('navigation-mode-changed', handleModeChange);
window.addEventListener('navigation-style-changed', handleStyleChange);
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function SettingsView({

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (event.key === 'Escape' && !event.defaultPrevented) {
onClose();
}
};
Expand Down
128 changes: 57 additions & 71 deletions ui/desktop/src/components/settings/dictation/DictationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,57 @@ 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>>(
{}
);
const [apiKey, setApiKey] = useState('');
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;

if (
DICTATION_ALLOWED_PROVIDERS &&
loadedProvider &&
!DICTATION_ALLOWED_PROVIDERS.includes(loadedProvider)
) {
loadedProvider = null;
await upsert('voice_dictation_provider', '', false);
}

const audioConfig = await getDictationConfig();
setProviderStatuses(audioConfig.data || {});
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];
Expand All @@ -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 () => {
Expand All @@ -78,21 +81,23 @@ export const DictationSettings = () => {
await remove(keyName, true);
setApiKey('');
setIsEditingKey(false);

const audioConfig = await getDictationConfig();
setProviderStatuses(audioConfig.data || {});
await refreshStatuses();
};

const handleCancelEdit = () => {
setApiKey('');
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">
Expand 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] && (
Expand Down
1 change: 1 addition & 0 deletions ui/desktop/src/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const COST_TRACKING_ENABLED = true;
export const ANNOUNCEMENTS_ENABLED = false;
export const CONFIGURATION_ENABLED = true;
export const TELEMETRY_UI_ENABLED = true;
export const DICTATION_ALLOWED_PROVIDERS: string[] | null = null;
Loading