diff --git a/ui/src/components/profiles/editor/friendly-ui-section.tsx b/ui/src/components/profiles/editor/friendly-ui-section.tsx index 2c7e6c08..109a3231 100644 --- a/ui/src/components/profiles/editor/friendly-ui-section.tsx +++ b/ui/src/components/profiles/editor/friendly-ui-section.tsx @@ -1,16 +1,22 @@ /** * Friendly UI Section * Left column with environment variables and info tabs - * Enhanced with OpenRouter model picker when applicable + * Enhanced with OpenRouter-specific streamlined UI when applicable */ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { EnvEditorSection } from './env-editor-section'; import { InfoSection } from './info-section'; import { OpenRouterModelPicker } from '@/components/profiles/openrouter-model-picker'; import { ModelTierMapping, type TierMapping } from '@/components/profiles/model-tier-mapping'; +import { Label } from '@/components/ui/label'; +import { MaskedInput } from '@/components/ui/masked-input'; +import { ChevronRight, Settings2 } from 'lucide-react'; import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './utils'; +import { toast } from 'sonner'; +import { cn } from '@/lib/utils'; import type { Settings, SettingsResponse } from './types'; interface FriendlyUISectionProps { @@ -46,9 +52,27 @@ export function FriendlyUISection({ // Memoize currentEnv for consistent reference const currentEnv = settingsEnv ?? {}; - // Handle model selection from OpenRouter picker + // Handle model selection from OpenRouter picker - applies to ALL tiers const handleModelChange = (modelId: string) => { - onEnvValueChange('ANTHROPIC_MODEL', modelId); + if (onEnvBulkChange) { + // Update all 4 model tiers at once + const newEnv = { + ...currentEnv, + ANTHROPIC_MODEL: modelId, + ANTHROPIC_DEFAULT_OPUS_MODEL: modelId, + ANTHROPIC_DEFAULT_SONNET_MODEL: modelId, + ANTHROPIC_DEFAULT_HAIKU_MODEL: modelId, + }; + onEnvBulkChange(newEnv); + } else { + // Fallback: update one by one + onEnvValueChange('ANTHROPIC_MODEL', modelId); + onEnvValueChange('ANTHROPIC_DEFAULT_OPUS_MODEL', modelId); + onEnvValueChange('ANTHROPIC_DEFAULT_SONNET_MODEL', modelId); + onEnvValueChange('ANTHROPIC_DEFAULT_HAIKU_MODEL', modelId); + } + // Show feedback toast + toast.success('Applied model to all tiers', { duration: 2000 }); }; // Handle tier mapping change @@ -71,13 +95,30 @@ export function FriendlyUISection({ } }; + // State for collapsible sections + const [showAllEnvVars, setShowAllEnvVars] = useState(false); + + // For OpenRouter: filter out model-related env vars from the main display + // These are managed by the model picker and tier mapping + const openRouterManagedKeys = new Set([ + 'ANTHROPIC_MODEL', + 'ANTHROPIC_DEFAULT_OPUS_MODEL', + 'ANTHROPIC_DEFAULT_SONNET_MODEL', + 'ANTHROPIC_DEFAULT_HAIKU_MODEL', + ]); + + // Count of hidden env vars for OpenRouter profiles + const hiddenEnvVarCount = isOpenRouter + ? Object.keys(currentEnv).filter((k) => openRouterManagedKeys.has(k)).length + : 0; + return ( -
- +
+
- Environment Variables + {isOpenRouter ? 'Configuration' : 'Environment Variables'} Info & Usage @@ -85,39 +126,114 @@ export function FriendlyUISection({
-
+
- {/* OpenRouter Model Picker Section */} - {isOpenRouter && ( -
-
- - -
- -
- )} + {/* OpenRouter Streamlined View */} + {isOpenRouter ? ( +
+
+ {/* Model Selection - Primary Focus */} +
+ + +
- + {/* Model Tier Mapping - Collapsible */} + + + {/* API Key - Simplified */} +
+ + onEnvValueChange('ANTHROPIC_AUTH_TOKEN', e.target.value)} + placeholder="sk-or-v1-..." + className="font-mono text-sm" + /> +

+ Get your API key from{' '} + + openrouter.ai/keys + +

+
+ + {/* Advanced: All Environment Variables */} + + + + + All Environment Variables + + ({Object.keys(currentEnv).length} vars + {hiddenEnvVarCount > 0 && `, ${hiddenEnvVarCount} managed by picker`}) + + + +
+ {Object.entries(currentEnv).map(([key, value]) => ( +
+ + {key === 'ANTHROPIC_AUTH_TOKEN' ? ( + onEnvValueChange(key, e.target.value)} + className="font-mono text-xs h-8" + /> + ) : ( + onEnvValueChange(key, e.target.value)} + className="w-full font-mono text-xs h-8 px-2 rounded border bg-background" + readOnly={openRouterManagedKeys.has(key)} + /> + )} +
+ ))} +
+
+
+
+
+ ) : ( + /* Standard Env Editor for non-OpenRouter profiles */ + + )}
) : (
-
+
+
{/* Search Header */}
@@ -155,7 +155,7 @@ export function OpenRouterModelPicker({ )} {/* Model List */} - + {isError ? (
Failed to load models.{' '} @@ -168,23 +168,25 @@ export function OpenRouterModelPicker({ No models found matching "{search}"
) : ( -
+
{/* Newest Models Section (shown when no search) */} {showPresets && newestModels.length > 0 && (
-
+
Newest Models
- {newestModels.map((model) => ( - onChange(model.id)} - showAge - /> - ))} +
+ {newestModels.map((model) => ( + onChange(model.id)} + showAge + /> + ))} +
)} @@ -195,17 +197,19 @@ export function OpenRouterModelPicker({ return (
-
+
{CATEGORY_LABELS[category]}
- {categoryModels.map((model) => ( - onChange(model.id)} - /> - ))} +
+ {categoryModels.map((model) => ( + onChange(model.id)} + /> + ))} +
); })} @@ -232,39 +236,49 @@ function ModelItem({ type="button" onClick={onClick} className={cn( - 'flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-left text-sm transition-colors', + 'group flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors', 'hover:bg-accent hover:text-accent-foreground', isSelected && 'bg-accent text-accent-foreground' )} > - {model.name} + {model.name} {showAge && model.created && ( {formatModelAge(model.created)} )} {model.isFree ? ( - + Free ) : ( - {formatPricingPair(model.pricing)} + {formatPricingPair(model.pricing)} )} - {formatContextLength(model.context_length)} + {formatContextLength(model.context_length)} ); diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index 5f17a911..30f46e30 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -164,6 +164,10 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr setValue('sonnetModel', model.id); setValue('haikuModel', model.id); setModelSearch(model.name); + // Show feedback that model was applied to all tiers + toast.success(`Applied "${model.name}" to all model tiers`, { + duration: 2000, + }); }; // Check for common URL mistakes - only for truly custom URLs @@ -401,6 +405,12 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr value={modelSearch} onChange={(e) => setModelSearch(e.target.value)} placeholder="Type to search (e.g., opus, sonnet, gpt-4o)..." + onKeyDown={(e) => { + if (e.key === 'Enter' && filteredModels.length > 0) { + e.preventDefault(); + handleModelSelect(filteredModels[0]); + } + }} />
{filteredModels.length === 0 ? ( @@ -581,17 +591,23 @@ function ModelSearchItem({