import { useQuery } from '@tanstack/react-query' import { useState } from 'react' import type { ModelOptionProvider, ModelOptionsResponse, ModelPricing } from '@/types/hermes' import type { HermesGateway } from '../hermes' import { getGlobalModelOptions } from '../hermes' import { cn } from '../lib/utils' import { InlineNotice } from './notifications' import { Button } from './ui/button' import { Checkbox } from './ui/checkbox' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/command' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog' import { Skeleton } from './ui/skeleton' interface ModelPickerDialogProps { open: boolean onOpenChange: (open: boolean) => void gw?: HermesGateway sessionId?: string | null currentModel: string currentProvider: string onSelect: (selection: { provider: string; model: string; persistGlobal: boolean }) => void /** * Optional class to apply to DialogContent. Use to override z-index when * stacking the picker on top of another fixed overlay (e.g. the desktop * onboarding overlay, which sits at z-1300; the default Dialog z-130 ends * up rendering underneath and blocks pointer events). */ contentClassName?: string } export function ModelPickerDialog({ open, onOpenChange, gw, sessionId, currentModel, currentProvider, onSelect, contentClassName }: ModelPickerDialogProps) { const [persistGlobal, setPersistGlobal] = useState(!sessionId) const modelOptions = useQuery({ queryKey: ['model-options', sessionId || 'global'], queryFn: () => { if (gw && sessionId) { return gw.request('model.options', { session_id: sessionId }) } return getGlobalModelOptions() }, enabled: open }) const providers = modelOptions.data?.providers ?? [] const optionsModel = String(modelOptions.data?.model ?? currentModel ?? '') const optionsProvider = String(modelOptions.data?.provider ?? currentProvider ?? '') const loading = modelOptions.isPending && !modelOptions.data const error = modelOptions.error ? modelOptions.error instanceof Error ? modelOptions.error.message : String(modelOptions.error) : null const selectModel = (provider: ModelOptionProvider, model: string) => { onSelect({ provider: provider.slug, model, persistGlobal: persistGlobal || !sessionId }) onOpenChange(false) } return ( Switch model current: {optionsModel || currentModel || '(unknown)'} {optionsProvider || currentProvider ? ` · ${optionsProvider || currentProvider}` : ''} {!loading && !error && No models found.} ) } function ModelResults({ loading, error, providers, currentModel, currentProvider, onSelectModel }: { loading: boolean error: string | null providers: ModelOptionProvider[] currentModel: string currentProvider: string onSelectModel: (provider: ModelOptionProvider, model: string) => void }) { if (loading) { return } if (error) { return (
{error}
) } if (providers.length === 0) { return
No authenticated providers.
} return ( <> {providers.map(provider => { const models = provider.models ?? [] if (models.length === 0) { return null } const unavailable = new Set(provider.unavailable_models ?? []) return ( } key={provider.slug}> {provider.warning && (
{provider.warning}
)} {models.map(model => { const isCurrent = model === currentModel && provider.slug === currentProvider const price = provider.pricing?.[model] const locked = unavailable.has(model) return ( { if (!locked) { onSelectModel(provider, model) } }} value={`${provider.name} ${provider.slug} ${model}`} > {model} {locked && Pro} ) })} {unavailable.size > 0 && (
Pro models need a paid Nous subscription.
)}
) })} ) } // Compact In/Out $/Mtok price tag, mirroring the CLI picker's price columns. // Renders nothing when pricing is unavailable for the model. function ModelPrice({ price, isCurrent }: { price?: ModelPricing; isCurrent: boolean }) { if (!price || (!price.input && !price.output)) { return null } if (price.free) { return ( Free ) } return ( {price.input || '?'} / {price.output || '?'} ) } function LoadingResults() { return ( }> {Array.from({ length: 4 }, (_, rowIndex) => (
))}
) } function ProviderHeading({ provider }: { provider: ModelOptionProvider }) { // free_tier is only set for Nous. true → "Free tier", false → "Pro". const tierBadge = provider.free_tier === true ? ( Free tier ) : provider.free_tier === false ? ( Pro ) : null return ( {provider.name} {provider.slug} · {provider.total_models ?? provider.models?.length ?? 0} {tierBadge} ) }