fix(profiles): reset form state on preset transitions

- resolve presets through canonical alias-aware lookup

- reset all tier model fields when switching presets/custom

- derive quick templates from non-recommended categories
This commit is contained in:
Tam Nhu Tran committed 2026-02-20 23:24:11 +07:00
1 parent 9ad8e64e85
commit 14242b5681
1 file changed
+76 -54
@@ -4,7 +4,7 @@
*/ */
/* eslint-disable react-hooks/set-state-in-effect */ /* eslint-disable react-hooks/set-state-in-effect */
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod'; import * as z from 'zod';
@@ -30,6 +30,7 @@ import { cn } from '@/lib/utils';
import { import {
PROVIDER_PRESETS, PROVIDER_PRESETS,
getPresetsByCategory, getPresetsByCategory,
getPresetById,
type ProviderPreset, type ProviderPreset,
} from '@/lib/provider-presets'; } from '@/lib/provider-presets';
import { import {
@@ -55,6 +56,7 @@ const schema = z.object({
}); });
type FormData = z.infer<typeof schema>; type FormData = z.infer<typeof schema>;
type PresetSelection = ProviderPreset['id'] | 'custom';
interface ProfileCreateDialogProps { interface ProfileCreateDialogProps {
open: boolean; open: boolean;
@@ -65,6 +67,26 @@ interface ProfileCreateDialogProps {
// Common URL mistakes to warn about // Common URL mistakes to warn about
const PROBLEMATIC_PATHS = ['/chat/completions', '/v1/messages', '/messages', '/completions']; const PROBLEMATIC_PATHS = ['/chat/completions', '/v1/messages', '/messages', '/completions'];
const CUSTOM_PRESET_ID = 'custom';
const DEFAULT_PRESET_ID: ProviderPreset['id'] = 'openrouter';
const EMPTY_FORM_VALUES: FormData = {
name: '',
baseUrl: '',
apiKey: '',
model: '',
opusModel: '',
sonnetModel: '',
haikuModel: '',
};
const RECOMMENDED_PRESETS = getPresetsByCategory('recommended');
const QUICK_TEMPLATE_PRESETS = PROVIDER_PRESETS.filter(
(preset) => preset.category !== 'recommended'
);
const QUICK_TEMPLATE_PRESET_IDS = new Set<string>(
QUICK_TEMPLATE_PRESETS.map((preset) => preset.id)
);
export function ProfileCreateDialog({ export function ProfileCreateDialog({
open, open,
@@ -76,7 +98,7 @@ export function ProfileCreateDialog({
const [activeTab, setActiveTab] = useState('basic'); const [activeTab, setActiveTab] = useState('basic');
const [urlWarning, setUrlWarning] = useState<string | null>(null); const [urlWarning, setUrlWarning] = useState<string | null>(null);
const [showApiKey, setShowApiKey] = useState(false); const [showApiKey, setShowApiKey] = useState(false);
const [selectedPreset, setSelectedPreset] = useState<string | null>('openrouter'); const [selectedPreset, setSelectedPreset] = useState<PresetSelection>(DEFAULT_PRESET_ID);
const [modelSearch, setModelSearch] = useState(''); const [modelSearch, setModelSearch] = useState('');
// OpenRouter models for model picker // OpenRouter models for model picker
@@ -91,23 +113,34 @@ export function ProfileCreateDialog({
setValue, setValue,
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: EMPTY_FORM_VALUES,
name: '',
baseUrl: '',
apiKey: '',
model: '',
opusModel: '',
sonnetModel: '',
haikuModel: '',
},
}); });
const baseUrlValue = useWatch({ control, name: 'baseUrl' }); const baseUrlValue = useWatch({ control, name: 'baseUrl' });
const applyPresetToForm = useCallback(
(preset: ProviderPreset | null) => {
if (!preset) {
reset(EMPTY_FORM_VALUES);
return;
}
reset({
...EMPTY_FORM_VALUES,
name: preset.defaultProfileName,
baseUrl: preset.baseUrl,
model: preset.defaultModel,
opusModel: preset.defaultModel,
sonnetModel: preset.defaultModel,
haikuModel: preset.defaultModel,
});
},
[reset]
);
// Get current preset config // Get current preset config
const currentPreset = useMemo(() => { const currentPreset = useMemo(() => {
if (!selectedPreset || selectedPreset === 'custom') return null; if (selectedPreset === CUSTOM_PRESET_ID) return null;
return PROVIDER_PRESETS.find((p) => p.id === selectedPreset); return getPresetById(selectedPreset) ?? null;
}, [selectedPreset]); }, [selectedPreset]);
// Filter models for OpenRouter search (newest first) // Filter models for OpenRouter search (newest first)
@@ -124,7 +157,6 @@ export function ProfileCreateDialog({
// Reset form when dialog opens // Reset form when dialog opens
useEffect(() => { useEffect(() => {
if (open) { if (open) {
reset();
setActiveTab('basic'); setActiveTab('basic');
setUrlWarning(null); setUrlWarning(null);
setShowApiKey(false); setShowApiKey(false);
@@ -132,45 +164,35 @@ export function ProfileCreateDialog({
// Set initial preset based on initialMode // Set initial preset based on initialMode
if (initialMode === 'normal') { if (initialMode === 'normal') {
// Custom mode - clear form setSelectedPreset(CUSTOM_PRESET_ID);
setSelectedPreset('custom'); applyPresetToForm(null);
setTimeout(() => {
setValue('name', '');
setValue('baseUrl', '');
}, 0);
} else { } else {
// OpenRouter mode (default) const defaultPreset = getPresetById(DEFAULT_PRESET_ID);
setSelectedPreset('openrouter'); if (defaultPreset) {
const openrouterPreset = PROVIDER_PRESETS.find((p) => p.id === 'openrouter'); setSelectedPreset(defaultPreset.id);
if (openrouterPreset) { applyPresetToForm(defaultPreset);
setTimeout(() => { return;
setValue('name', openrouterPreset.defaultProfileName);
setValue('baseUrl', openrouterPreset.baseUrl);
}, 0);
} }
// Safe fallback if default preset is missing.
setSelectedPreset(CUSTOM_PRESET_ID);
applyPresetToForm(null);
} }
} }
}, [open, reset, setValue, initialMode]); }, [open, initialMode, applyPresetToForm]);
// Handle preset selection // Handle preset selection
const handlePresetSelect = (presetId: string) => { const handlePresetSelect = (presetId: string) => {
setSelectedPreset(presetId); const preset = getPresetById(presetId);
const preset = PROVIDER_PRESETS.find((p) => p.id === presetId);
if (preset) { if (preset) {
setValue('name', preset.defaultProfileName); setSelectedPreset(preset.id);
setValue('baseUrl', preset.baseUrl); applyPresetToForm(preset);
if (preset.defaultModel) { return;
setValue('model', preset.defaultModel);
setValue('opusModel', preset.defaultModel);
setValue('sonnetModel', preset.defaultModel);
setValue('haikuModel', preset.defaultModel);
}
} else {
// Custom
setValue('name', '');
setValue('baseUrl', '');
setValue('model', '');
} }
setSelectedPreset(CUSTOM_PRESET_ID);
applyPresetToForm(null);
}; };
// Handle model selection from picker - applies to all 4 model tiers // Handle model selection from picker - applies to all 4 model tiers
@@ -190,7 +212,7 @@ export function ProfileCreateDialog({
// Presets (OpenRouter, GLM, GLMT, Kimi) have vetted URLs that may require full paths // Presets (OpenRouter, GLM, GLMT, Kimi) have vetted URLs that may require full paths
useEffect(() => { useEffect(() => {
// Only warn for custom URLs, not preset-selected ones // Only warn for custom URLs, not preset-selected ones
const isCustomUrl = selectedPreset === 'custom'; const isCustomUrl = selectedPreset === CUSTOM_PRESET_ID;
if (baseUrlValue && isCustomUrl) { if (baseUrlValue && isCustomUrl) {
const lowerUrl = baseUrlValue.toLowerCase(); const lowerUrl = baseUrlValue.toLowerCase();
for (const path of PROBLEMATIC_PATHS) { for (const path of PROBLEMATIC_PATHS) {
@@ -232,7 +254,9 @@ export function ProfileCreateDialog({
const hasModelErrors = const hasModelErrors =
!!errors.model || !!errors.opusModel || !!errors.sonnetModel || !!errors.haikuModel; !!errors.model || !!errors.opusModel || !!errors.sonnetModel || !!errors.haikuModel;
const isOpenRouter = selectedPreset === 'openrouter'; const isQuickTemplateSelected =
selectedPreset !== CUSTOM_PRESET_ID && QUICK_TEMPLATE_PRESET_IDS.has(selectedPreset);
const isOpenRouter = currentPreset?.id === DEFAULT_PRESET_ID;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -257,7 +281,7 @@ export function ProfileCreateDialog({
<div> <div>
<Label className="text-xs text-muted-foreground mb-1.5 block">Provider</Label> <Label className="text-xs text-muted-foreground mb-1.5 block">Provider</Label>
<div className="flex gap-2"> <div className="flex gap-2">
{getPresetsByCategory('recommended').map((preset) => ( {RECOMMENDED_PRESETS.map((preset) => (
<CompactPresetCard <CompactPresetCard
key={preset.id} key={preset.id}
preset={preset} preset={preset}
@@ -268,11 +292,10 @@ export function ProfileCreateDialog({
{/* Custom option */} {/* Custom option */}
<button <button
type="button" type="button"
onClick={() => handlePresetSelect('custom')} onClick={() => handlePresetSelect(CUSTOM_PRESET_ID)}
className={cn( className={cn(
'flex items-center gap-2 px-4 py-2 rounded-md border-2 transition-all text-sm font-medium', 'flex items-center gap-2 px-4 py-2 rounded-md border-2 transition-all text-sm font-medium',
selectedPreset === 'custom' || selectedPreset === CUSTOM_PRESET_ID || isQuickTemplateSelected
getPresetsByCategory('alternative').some((p) => p.id === selectedPreset)
? 'border-primary bg-primary/10 text-primary dark:bg-primary/20' ? 'border-primary bg-primary/10 text-primary dark:bg-primary/20'
: 'border-dashed border-muted-foreground/40 hover:border-primary/50 hover:bg-muted/50 text-muted-foreground hover:text-foreground' : 'border-dashed border-muted-foreground/40 hover:border-primary/50 hover:bg-muted/50 text-muted-foreground hover:text-foreground'
)} )}
@@ -283,15 +306,14 @@ export function ProfileCreateDialog({
</div> </div>
</div> </div>
{/* Show alternative presets when Custom is selected or an alternative is selected */} {/* Show quick templates when custom mode or non-recommended preset is selected */}
{(selectedPreset === 'custom' || {(selectedPreset === CUSTOM_PRESET_ID || isQuickTemplateSelected) && (
getPresetsByCategory('alternative').some((p) => p.id === selectedPreset)) && (
<div className="pt-3 mt-2 border-t border-dashed border-muted-foreground/30"> <div className="pt-3 mt-2 border-t border-dashed border-muted-foreground/30">
<Label className="text-xs font-medium text-foreground/70 mb-2 block"> <Label className="text-xs font-medium text-foreground/70 mb-2 block">
Quick Templates Quick Templates
</Label> </Label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{getPresetsByCategory('alternative').map((preset) => ( {QUICK_TEMPLATE_PRESETS.map((preset) => (
<CompactPresetCard <CompactPresetCard
key={preset.id} key={preset.id}
preset={preset} preset={preset}