From 96310dd1acd5d6265cd2d68d970b79409d988c1f Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 20 Dec 2025 21:09:01 -0500 Subject: [PATCH] refactor(ui): reorganize profile create dialog with preset categories - Split presets into recommended (OpenRouter) + alternative (GLM/GLMT/Kimi) - Show alternative presets only when Custom is selected - Replace PresetCard with CompactPresetCard (horizontal layout) - Make baseUrl always editable (pre-filled from preset but customizable) - Apply model selection to all 4 model tiers (opus/sonnet/haiku) - Only show URL path warnings for truly custom URLs, not preset URLs - Use accent tokens for consistent theming --- .../profiles/profile-create-dialog.tsx | 183 ++++++++++-------- 1 file changed, 102 insertions(+), 81 deletions(-) diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index ecd6db70..5f17a911 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -27,7 +27,11 @@ import { useOpenRouterCatalog } from '@/hooks/use-openrouter-models'; import { Loader2, Plus, AlertTriangle, Info, Eye, EyeOff, Settings2, Sparkles } from 'lucide-react'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; -import { PROVIDER_PRESETS, type ProviderPreset } from '@/lib/provider-presets'; +import { + PROVIDER_PRESETS, + getPresetsByCategory, + type ProviderPreset, +} from '@/lib/provider-presets'; import { searchModels, formatPricingPair, @@ -153,15 +157,21 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr } }; - // Handle model selection from picker + // Handle model selection from picker - applies to all 4 model tiers const handleModelSelect = (model: CategorizedModel) => { setValue('model', model.id); + setValue('opusModel', model.id); + setValue('sonnetModel', model.id); + setValue('haikuModel', model.id); setModelSearch(model.name); }; - // Check for common URL mistakes + // Check for common URL mistakes - only for truly custom URLs + // Presets (OpenRouter, GLM, GLMT, Kimi) have vetted URLs that may require full paths useEffect(() => { - if (baseUrlValue && selectedPreset === 'custom') { + // Only warn for custom URLs, not preset-selected ones + const isCustomUrl = selectedPreset === 'custom'; + if (baseUrlValue && isCustomUrl) { const lowerUrl = baseUrlValue.toLowerCase(); for (const path of PROBLEMATIC_PATHS) { if (lowerUrl.endsWith(path)) { @@ -177,10 +187,9 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr }, [baseUrlValue, selectedPreset]); const onSubmit = async (data: FormData) => { - const preset = currentPreset; + // Use user-provided baseUrl (allows customization of preset URLs) const finalData = { ...data, - baseUrl: preset ? preset.baseUrl : data.baseUrl, }; try { await createMutation.mutateAsync(finalData); @@ -212,33 +221,57 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr
- {/* Provider Preset Cards */} -
- -
- {PROVIDER_PRESETS.map((preset) => ( - handlePresetSelect(preset.id)} - /> - ))} - {/* Custom option */} - + {/* Provider Preset Cards - Compact horizontal layout */} +
+ {/* Main Options: OpenRouter + Custom */} +
+ +
+ {getPresetsByCategory('recommended').map((preset) => ( + handlePresetSelect(preset.id)} + /> + ))} + {/* Custom option */} + +
+ + {/* Show alternative presets when Custom is selected or an alternative is selected */} + {(selectedPreset === 'custom' || + getPresetsByCategory('alternative').some((p) => p.id === selectedPreset)) && ( +
+ +
+ {getPresetsByCategory('alternative').map((preset) => ( + handlePresetSelect(preset.id)} + /> + ))} +
+
+ )}
- {/* Base URL - only show for custom */} - {selectedPreset === 'custom' ? ( -
- - - {errors.baseUrl ? ( -

{errors.baseUrl.message}

- ) : urlWarning ? ( -
- - {urlWarning} -
- ) : ( -

- The endpoint that accepts OpenAI-compatible and Anthropic requests -

- )} -
- ) : ( - currentPreset && ( -
- {currentPreset.icon ? ( - - ) : ( - - )} -
-

{currentPreset.name} API

-

{currentPreset.baseUrl}

-
+ {/* Base URL - always editable, pre-filled from preset */} +
+ + + {errors.baseUrl ? ( +

{errors.baseUrl.message}

+ ) : urlWarning ? ( +
+ + {urlWarning}
- ) - )} + ) : currentPreset ? ( +

+ Pre-filled from {currentPreset.name}. You can customize if needed. +

+ ) : ( +

+ The endpoint that accepts OpenAI-compatible and Anthropic requests +

+ )} +
{/* API Key */}
@@ -392,7 +413,7 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr
{!modelSearch && (
- + Newest Models
)} @@ -506,8 +527,8 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr ); } -/** Preset card component */ -function PresetCard({ +/** Compact preset card component - horizontal layout */ +function CompactPresetCard({ preset, isSelected, onClick, @@ -521,24 +542,24 @@ function PresetCard({ type="button" onClick={onClick} className={cn( - 'flex flex-col items-center gap-1 p-3 rounded-lg border-2 transition-all text-center', + 'flex items-center gap-1.5 px-3 py-1.5 rounded-md border transition-all text-sm', isSelected ? preset.featured - ? 'border-orange-500 bg-orange-50 dark:bg-orange-950/20' - : 'border-primary bg-primary/5' + ? 'border-accent bg-accent/10 dark:bg-accent/20 font-medium' + : 'border-primary bg-primary/5 font-medium' : 'border-muted hover:border-muted-foreground/30' )} > {preset.icon ? ( - + ) : ( -
+
{preset.name.charAt(0)}
)} - {preset.name} + {preset.name} {preset.badge && ( - + {preset.badge} )} @@ -565,7 +586,7 @@ function ModelSearchItem({ {model.name} {showAge && model.created && ( - + {formatModelAge(model.created)} )}