97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
/*
|
|
* @Author: ZhaoYing
|
|
* @Date: 2026-03-07 16:49:59
|
|
* @Last Modified by: ZhaoYing
|
|
* @Last Modified time: 2026-04-17 10:05:23
|
|
*/
|
|
import { type FC, useEffect, useState } from 'react';
|
|
import { Select, Flex, Space } from 'antd';
|
|
import type { SelectProps } from 'antd/es/select';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { getModelList } from '@/api/models';
|
|
import type { Query, Model } from '@/views/ModelManagement/types';
|
|
import { getListLogoUrl } from '@/views/ModelManagement/utils';
|
|
import Tag from '@/components/Tag';
|
|
|
|
export interface ModelSelectRef {
|
|
options: Model[];
|
|
}
|
|
|
|
/** Extends AntD SelectProps; omits filterOption since it's handled internally */
|
|
interface ModelSelectProps extends SelectProps {
|
|
/** Extra query params passed to getModelList */
|
|
params?: Query;
|
|
placeholder?: string;
|
|
fontClassName?: string;
|
|
isAutoFetch?: boolean;
|
|
initialData?: Model[];
|
|
updateOptions?: (options: Model[]) => void;
|
|
}
|
|
|
|
const ModelSelect: FC<ModelSelectProps> = ({ params, placeholder, fontClassName, isAutoFetch = true, initialData = [], updateOptions, ...props }) => {
|
|
const { t } = useTranslation();
|
|
const [options, setOptions] = useState<Model[]>([]);
|
|
|
|
// Fetch active models whenever params change; stringify for stable deep comparison
|
|
useEffect(() => {
|
|
if (!isAutoFetch) return
|
|
getModelList({
|
|
...(params ?? {}),
|
|
pagesize: 100,
|
|
is_active: true
|
|
}).then((res) => {
|
|
setOptions((res as { items: Model[] }).items ?? []);
|
|
});
|
|
}, [JSON.stringify(params), isAutoFetch]);
|
|
|
|
// Render the selected value inside the trigger with logo + truncated name
|
|
const labelRender: SelectProps['labelRender'] = ({ value }) => {
|
|
const item = options.find((o) => o.id === value);
|
|
if (!item) return undefined;
|
|
const logo = getListLogoUrl(item.provider, item.logo as string);
|
|
return (
|
|
<Flex align="center" gap={8}>
|
|
{logo && <img src={logo} className="rb:size-5 rb:rounded-md" alt={logo} />}
|
|
<div className={`rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap ${fontClassName}`}>{item.name}</div>
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (updateOptions) updateOptions([...options, ...initialData]);
|
|
}, [options, initialData])
|
|
|
|
return (
|
|
<Select
|
|
placeholder={placeholder ?? t('common.pleaseSelect')}
|
|
options={[...options, ...initialData]}
|
|
fieldNames={{ label: 'name', value: 'id' }}
|
|
allowClear
|
|
popupMatchSelectWidth={false}
|
|
labelRender={labelRender}
|
|
// Each dropdown option shows logo, name, and capability tags
|
|
optionRender={(option) => {
|
|
const { data } = option;
|
|
const logo = getListLogoUrl(data.provider, data.logo as string);
|
|
return (
|
|
<Flex align="center" gap={8}>
|
|
<Flex align="center" gap={8}>
|
|
{logo && <img src={logo} className="rb:size-5 rb:rounded-md" alt={logo} />}
|
|
<span className="rb:wrap-break-word rb:line-clamp-1">{data.name as string}</span>
|
|
</Flex>
|
|
{data.capability?.length > 0 && (
|
|
<Space size={4}>
|
|
{data.capability.map((vo: string) => <Tag key={vo}>{vo}</Tag>)}
|
|
</Space>
|
|
)}
|
|
</Flex>
|
|
);
|
|
}}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default ModelSelect;
|