import { Check, ChevronLeft, ChevronRight, Loader2, Pin, Search, Star } from 'lucide-react';
import React, { useEffect, useMemo, useState } from 'react';
import { useI18n } from '../../application/i18n/I18nProvider';
import {
filterComposerModels,
resolveComposerEnterModelId,
resolvePinnedAndRecentModels,
type ComposerModelPrefEntry,
type ComposerModelPrefs,
type ComposerPickerModel,
} from '../../infrastructure/ai/composerPicker';
import type { AgentModelPreset, ProviderConfig } from '../../infrastructure/ai/types';
import { ProviderIconBadge } from '../settings/tabs/ai/ProviderIconBadge';
import { useProviderModelCatalog } from './useProviderModelCatalog';
export const COMPOSER_PROVIDER_PICKER_WIDTH = 260;
export const COMPOSER_MODEL_PICKER_WIDTH = 260;
export interface ComposerModelPickerProps {
providers?: ProviderConfig[];
selectedProviderId?: string;
selectedModelId?: string;
modelPresets?: AgentModelPreset[];
prefs: ComposerModelPrefs;
onSelectProviderModel?: (providerId: string, modelId: string, contextWindow?: number) => void;
onSelectModel?: (modelId: string) => void;
onTogglePinned: (entry: ComposerModelPrefEntry) => void;
}
const rowClassName =
'flex h-8 w-full items-center gap-2 px-2.5 text-left text-[12px] hover:bg-muted/30 transition-colors cursor-pointer';
const SectionLabel: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
);
const ModelRow: React.FC<{
model: ComposerPickerModel;
selected: boolean;
pinned: boolean;
onSelect: () => void;
onTogglePinned: () => void;
pinLabel: string;
unpinLabel: string;
}> = ({ model, selected, pinned, onSelect, onTogglePinned, pinLabel, unpinLabel }) => (
);
export const ComposerModelPicker: React.FC = ({
providers = [],
selectedProviderId,
selectedModelId,
modelPresets = [],
prefs,
onSelectProviderModel,
onSelectModel,
onTogglePinned,
}) => {
const { t } = useI18n();
const hasProviders = providers.length > 0;
const [previewProviderId, setPreviewProviderId] = useState(
selectedProviderId || providers[0]?.id || '',
);
const [query, setQuery] = useState('');
const [view, setView] = useState<'models' | 'providers'>('models');
useEffect(() => {
if (selectedProviderId) setPreviewProviderId(selectedProviderId);
}, [selectedProviderId]);
const previewProvider = hasProviders
? providers.find((provider) => provider.id === previewProviderId) ?? providers[0]
: undefined;
const catalog = useProviderModelCatalog(previewProvider, hasProviders);
const models = useMemo(() => {
if (hasProviders) return catalog.models;
return modelPresets.map((preset) => ({
id: preset.id,
name: preset.name,
description: preset.description,
}));
}, [catalog.models, hasProviders, modelPresets]);
const filtered = useMemo(() => filterComposerModels(models, query), [models, query]);
const grouped = useMemo(
() => resolvePinnedAndRecentModels({
models: filtered,
prefs,
providerId: previewProvider?.id,
allowMissing: Boolean(previewProvider?.id) && !query.trim(),
}),
[filtered, prefs, previewProvider?.id, query],
);
const pinnedKeys = useMemo(
() => new Set(
prefs.pinned
.filter((entry) => !previewProvider || !entry.providerId || entry.providerId === previewProvider.id)
.map((entry) => entry.modelId),
),
[prefs.pinned, previewProvider],
);
const trimmedQuery = query.trim();
const showCustom = Boolean(
hasProviders
&& trimmedQuery
&& !models.some((model) => model.id.toLowerCase() === trimmedQuery.toLowerCase()),
);
const selectModel = (modelId: string) => {
const contextWindow = models.find((model) => model.id === modelId)?.contextWindow;
if (hasProviders && previewProvider) {
onSelectProviderModel?.(previewProvider.id, modelId, contextWindow);
return;
}
onSelectModel?.(modelId);
};
const prefEntryFor = (modelId: string): ComposerModelPrefEntry => (
previewProvider ? { providerId: previewProvider.id, modelId } : { modelId }
);
if (hasProviders && view === 'providers') {
return (
{providers.map((provider) => {
const isBound = provider.id === selectedProviderId;
return (
);
})}
);
}
return (
{hasProviders && previewProvider && (
<>
>
)}
{catalog.loading && (
{t('ai.chat.loadingModels')}
)}
{showCustom && (
)}
{grouped.pinned.length > 0 && (
<>
{t('ai.chat.pinned')}
{grouped.pinned.map((model) => (
selectModel(model.id)}
onTogglePinned={() => onTogglePinned(prefEntryFor(model.id))}
pinLabel={t('ai.chat.pinModel')}
unpinLabel={t('ai.chat.unpinModel')}
/>
))}
>
)}
{grouped.recent.length > 0 && (
<>
{t('ai.chat.recent')}
{grouped.recent.map((model) => (
selectModel(model.id)}
onTogglePinned={() => onTogglePinned(prefEntryFor(model.id))}
pinLabel={t('ai.chat.pinModel')}
unpinLabel={t('ai.chat.unpinModel')}
/>
))}
>
)}
{(grouped.pinned.length > 0 || grouped.recent.length > 0) && grouped.rest.length > 0 && (
{t('ai.chat.models')}
)}
{grouped.rest.map((model) => (
selectModel(model.id)}
onTogglePinned={() => onTogglePinned(prefEntryFor(model.id))}
pinLabel={t('ai.chat.pinModel')}
unpinLabel={t('ai.chat.unpinModel')}
/>
))}
{!catalog.loading && filtered.length === 0 && !showCustom && (
{catalog.error || t('ai.chat.noMatchingModels')}
)}
);
};
export default React.memo(ComposerModelPicker);