bolt.diy/app/components/chat/ModelSelector.tsx

190 lines
7.2 KiB
TypeScript
Raw Normal View History

2024-12-04 18:51:27 +00:00
import type { ProviderInfo } from '~/types/model';
2025-02-15 12:37:35 +00:00
import { useEffect, useState, useRef } from 'react';
import type { ModelInfo } from '~/lib/modules/llm/types';
2025-02-15 12:37:35 +00:00
import { classNames } from '~/utils/classNames';
2024-12-04 18:51:27 +00:00
interface ModelSelectorProps {
model?: string;
setModel?: (model: string) => void;
provider?: ProviderInfo;
setProvider?: (provider: ProviderInfo) => void;
modelList: ModelInfo[];
providerList: ProviderInfo[];
apiKeys: Record<string, string>;
modelLoading?: string;
2024-12-04 18:51:27 +00:00
}
export const ModelSelector = ({
model,
setModel,
provider,
setProvider,
modelList,
providerList,
modelLoading,
2024-12-04 18:51:27 +00:00
}: ModelSelectorProps) => {
2025-02-15 12:37:35 +00:00
const [modelSearchQuery, setModelSearchQuery] = useState('');
const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
// Filter models based on search query
const filteredModels = [...modelList]
.filter((e) => e.provider === provider?.name && e.name)
.filter(
(model) =>
model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()),
);
// Focus search input when dropdown opens
useEffect(() => {
if (isModelDropdownOpen && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [isModelDropdownOpen]);
2024-12-07 15:27:50 +00:00
// Update enabled providers when cookies change
useEffect(() => {
2024-12-13 06:31:50 +00:00
// If current provider is disabled, switch to first enabled provider
2025-02-15 12:37:35 +00:00
if (providerList.length === 0) {
2024-12-13 06:31:50 +00:00
return;
}
2024-12-07 15:27:50 +00:00
2024-12-13 06:31:50 +00:00
if (provider && !providerList.map((p) => p.name).includes(provider.name)) {
const firstEnabledProvider = providerList[0];
setProvider?.(firstEnabledProvider);
2024-12-07 15:53:33 +00:00
2024-12-13 06:31:50 +00:00
// Also update the model to the first available one for the new provider
const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);
2024-12-07 15:53:33 +00:00
2024-12-13 06:31:50 +00:00
if (firstModel) {
setModel?.(firstModel.name);
2024-12-07 15:27:50 +00:00
}
2024-12-13 06:31:50 +00:00
}
2024-12-07 15:27:50 +00:00
}, [providerList, provider, setProvider, modelList, setModel]);
2024-12-13 06:31:50 +00:00
if (providerList.length === 0) {
2024-12-07 15:27:50 +00:00
return (
<div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
<p className="text-center">
2024-12-07 15:53:33 +00:00
No providers are currently enabled. Please enable at least one provider in the settings to start using the
chat.
2024-12-07 15:27:50 +00:00
</p>
</div>
);
}
2024-12-04 18:51:27 +00:00
return (
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
<select
value={provider?.name ?? ''}
onChange={(e) => {
2024-12-13 06:31:50 +00:00
const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
2024-12-04 18:51:27 +00:00
if (newProvider && setProvider) {
setProvider(newProvider);
}
const firstModel = [...modelList].find((m) => m.provider === e.target.value);
if (firstModel && setModel) {
setModel(firstModel.name);
}
}}
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
>
2024-12-13 06:31:50 +00:00
{providerList.map((provider: ProviderInfo) => (
2024-12-04 18:51:27 +00:00
<option key={provider.name} value={provider.name}>
{provider.name}
</option>
))}
</select>
2025-02-15 12:37:35 +00:00
<div className="relative flex-1 lg:max-w-[70%]">
<div
className={classNames(
'w-full p-2 rounded-lg border border-bolt-elements-borderColor',
'bg-bolt-elements-prompt-background text-bolt-elements-textPrimary',
'focus-within:outline-none focus-within:ring-2 focus-within:ring-bolt-elements-focus',
'transition-all cursor-pointer',
isModelDropdownOpen ? 'ring-2 ring-bolt-elements-focus' : undefined,
)}
onClick={() => setIsModelDropdownOpen(!isModelDropdownOpen)}
>
<div className="flex items-center justify-between">
<span>{modelList.find((m) => m.name === model)?.label || 'Select model'}</span>
<span
className={classNames(
'i-ph:caret-down transition-transform',
isModelDropdownOpen ? 'rotate-180' : undefined,
)}
/>
</div>
</div>
{isModelDropdownOpen && (
<div className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background shadow-lg">
<div className="px-2 pb-2">
<div className="relative">
<input
ref={searchInputRef}
type="text"
value={modelSearchQuery}
onChange={(e) => setModelSearchQuery(e.target.value)}
placeholder="Search models..."
className={classNames(
'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
'transition-all',
)}
onClick={(e) => e.stopPropagation()}
/>
<div className="absolute left-2.5 top-1/2 -translate-y-1/2">
<span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />
</div>
</div>
</div>
<div
className={classNames(
'max-h-60 overflow-y-auto',
'scrollbar-thin scrollbar-track-bolt-elements-background-depth-2',
'scrollbar-thumb-bolt-elements-borderColor hover:scrollbar-thumb-bolt-elements-borderColorHover',
'scrollbar-thumb-rounded-full scrollbar-track-rounded-full',
)}
>
{modelLoading === 'all' || modelLoading === provider?.name ? (
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">Loading...</div>
) : filteredModels.length === 0 ? (
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">No models found</div>
) : (
filteredModels.map((modelOption, index) => (
<div
key={index}
className={classNames(
'px-3 py-2 text-sm cursor-pointer',
'hover:bg-bolt-elements-background-depth-3',
'text-bolt-elements-textPrimary',
model === modelOption.name ? 'bg-bolt-elements-background-depth-2' : undefined,
)}
onClick={(e) => {
e.stopPropagation();
setModel?.(modelOption.name);
setIsModelDropdownOpen(false);
setModelSearchQuery('');
}}
>
{modelOption.label}
</div>
))
)}
</div>
</div>
)}
2025-02-15 12:37:35 +00:00
</div>
2024-12-04 18:51:27 +00:00
</div>
);
};