Skip to content

Commit e59635f

Browse files
committed
✨ feat: support display model list
1 parent cef3f8c commit e59635f

File tree

7 files changed

+164
-66
lines changed

7 files changed

+164
-66
lines changed

src/app/settings/llm/OpenAI/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useGlobalStore } from '@/store/global';
1010
import { modelProviderSelectors } from '@/store/global/selectors';
1111

1212
import Checker from '../components/Checker';
13+
import CustomModelSelect from '../components/CustomModelList';
1314
import ProviderConfig from '../components/ProviderConfig';
1415
import { LLMProviderConfigKey } from '../const';
1516

@@ -66,10 +67,9 @@ const LLM = memo(() => {
6667
},
6768
{
6869
children: (
69-
<Input.TextArea
70-
allowClear
70+
<CustomModelSelect
7171
placeholder={t('llm.openai.customModelName.placeholder')}
72-
style={{ height: 100 }}
72+
provider={'openai'}
7373
/>
7474
),
7575
desc: t('llm.openai.customModelName.desc'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Typography } from 'antd';
2+
import isEqual from 'fast-deep-equal';
3+
import { memo } from 'react';
4+
import { Flexbox } from 'react-layout-kit';
5+
6+
import ModelIcon from '@/components/ModelIcon';
7+
import { ModelInfoTags } from '@/components/ModelSelect';
8+
import { useGlobalStore } from '@/store/global';
9+
import { modelProviderSelectors } from '@/store/global/slices/settings/selectors';
10+
11+
export const OptionRender = memo<{ displayName: string; id: string }>(({ displayName, id: id }) => {
12+
const model = useGlobalStore((s) => modelProviderSelectors.modelCardById(id)(s), isEqual);
13+
14+
return (
15+
<Flexbox align={'center'} gap={8} horizontal>
16+
<ModelIcon model={id} size={32} />
17+
<Flexbox>
18+
<Flexbox align={'center'} gap={8} horizontal>
19+
{displayName}
20+
<ModelInfoTags directionReverse placement={'top'} {...model!} />
21+
</Flexbox>
22+
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
23+
{id}
24+
</Typography.Text>
25+
</Flexbox>
26+
</Flexbox>
27+
);
28+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Select } from 'antd';
2+
import { css, cx } from 'antd-style';
3+
import isEqual from 'fast-deep-equal';
4+
import { memo } from 'react';
5+
6+
import { useGlobalStore } from '@/store/global';
7+
import { modelProviderSelectors } from '@/store/global/selectors';
8+
9+
import { OptionRender } from './Option';
10+
11+
const popup = css`
12+
&.ant-select-dropdown {
13+
.ant-select-item-option-selected {
14+
font-weight: normal;
15+
}
16+
}
17+
`;
18+
19+
interface CustomModelSelectProps {
20+
placeholder?: string;
21+
provider: string;
22+
}
23+
24+
const CustomModelSelect = memo<CustomModelSelectProps>(({ provider, placeholder }) => {
25+
const providerCard = useGlobalStore(
26+
(s) => modelProviderSelectors.modelSelectList(s).find((s) => s.id === provider),
27+
isEqual,
28+
);
29+
const defaultEnableModel = providerCard?.chatModels.filter((v) => !v.hidden).map((m) => m.id);
30+
31+
return (
32+
<Select
33+
allowClear
34+
defaultValue={defaultEnableModel}
35+
mode="tags"
36+
optionFilterProp="label"
37+
optionRender={({ label, value }) => (
38+
<OptionRender displayName={label as string} id={value as string} />
39+
)}
40+
options={providerCard?.chatModels.map((model) => ({
41+
label: model.displayName || model.id,
42+
value: model.id,
43+
}))}
44+
placeholder={placeholder}
45+
popupClassName={cx(popup)}
46+
popupMatchSelectWidth={false}
47+
/>
48+
);
49+
});
50+
51+
export default CustomModelSelect;

src/app/settings/llm/components/ProviderConfig/index.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { modelProviderSelectors } from '@/store/global/selectors';
1717
import { GlobalLLMProviderKey } from '@/types/settings';
1818

1919
import Checker from '../Checker';
20+
import CustomModelSelect from '../CustomModelList';
2021

2122
interface ProviderConfigProps {
2223
canDeactivate?: boolean;
@@ -33,7 +34,7 @@ interface ProviderConfigProps {
3334
const ProviderConfig = memo<ProviderConfigProps>(
3435
({
3536
provider,
36-
showCustomModelName,
37+
showCustomModelName = true,
3738
showEndpoint,
3839
showApiKey = true,
3940
checkModel,
@@ -74,14 +75,13 @@ const ProviderConfig = memo<ProviderConfigProps>(
7475
},
7576
showCustomModelName && {
7677
children: (
77-
<Input.TextArea
78-
allowClear
78+
<CustomModelSelect
7979
placeholder={t(`llm.${provider}.customModelName.placeholder` as any)}
80-
style={{ height: 100 }}
80+
provider={provider}
8181
/>
8282
),
83-
desc: t(`llm.${provider}.customModelName.desc` as any),
84-
label: t(`llm.${provider}.customModelName.title` as any),
83+
desc: t('llm.modelList.desc'),
84+
label: t('llm.modelList.title'),
8585
name: [LLMProviderConfigKey, provider, LLMProviderCustomModelKey],
8686
},
8787
checkerItem ?? {
@@ -113,6 +113,7 @@ const ProviderConfig = memo<ProviderConfigProps>(
113113
items={[model]}
114114
onValuesChange={debounce(setSettings, 100)}
115115
{...FORM_STYLE}
116+
itemMinWidth={'max(50%,400px)'}
116117
/>
117118
);
118119
},

src/components/ModelIcon/index.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ interface ModelProviderIconProps {
2222
size?: number;
2323
}
2424

25-
const ModelIcon = memo<ModelProviderIconProps>(({ model, size = 12 }) => {
26-
if (!model) return;
25+
const ModelIcon = memo<ModelProviderIconProps>(({ model: originModel, size = 12 }) => {
26+
if (!originModel) return;
27+
28+
// lower case the origin model so to better match more model id case
29+
const model = originModel.toLowerCase();
2730

2831
if (model.startsWith('gpt-3')) return <OpenAI.Avatar size={size} type={'gpt3'} />;
2932
if (model.startsWith('gpt-4')) return <OpenAI.Avatar size={size} type={'gpt4'} />;
@@ -41,7 +44,7 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model, size = 12 }) => {
4144
return <Baichuan.Avatar background={Baichuan.colorPrimary} size={size} />;
4245
if (model.includes('mistral') || model.includes('mixtral')) return <Mistral.Avatar size={size} />;
4346
if (model.includes('pplx')) return <Perplexity.Avatar size={size} />;
44-
if (model.startsWith('yi-')) return <Yi.Avatar size={size} />;
47+
if (model.includes('yi-')) return <Yi.Avatar size={size} />;
4548
});
4649

4750
export default ModelIcon;

src/components/ModelSelect/index.tsx

+65-54
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import { rgba } from 'polished';
66
import { memo } from 'react';
77
import { useTranslation } from 'react-i18next';
88
import { Center, Flexbox } from 'react-layout-kit';
9-
import ModelIcon from 'src/components/ModelIcon';
10-
import ModelProviderIcon from 'src/components/ModelProviderIcon';
119

1210
import { ChatModelCard } from '@/types/llm';
1311

12+
import ModelIcon from '../ModelIcon';
13+
import ModelProviderIcon from '../ModelProviderIcon';
14+
1415
const useStyles = createStyles(({ css, token }) => ({
1516
custom: css`
1617
width: 36px;
@@ -56,69 +57,79 @@ const useStyles = createStyles(({ css, token }) => ({
5657
`,
5758
}));
5859

60+
interface ModelInfoTagsProps extends ChatModelCard {
61+
directionReverse?: boolean;
62+
placement?: 'top' | 'right';
63+
}
64+
export const ModelInfoTags = memo<ModelInfoTagsProps>(
65+
({ directionReverse, placement = 'right', ...model }) => {
66+
const { t } = useTranslation('common');
67+
const { styles, cx } = useStyles();
68+
69+
return (
70+
<Flexbox direction={directionReverse ? 'horizontal-reverse' : 'horizontal'} gap={4}>
71+
{model.files && (
72+
<Tooltip placement={placement} title={t('ModelSelect.featureTag.file')}>
73+
<div className={cx(styles.tag, styles.tagGreen)}>
74+
<Icon icon={LucidePaperclip} />
75+
</div>
76+
</Tooltip>
77+
)}
78+
{model.vision && (
79+
<Tooltip placement={placement} title={t('ModelSelect.featureTag.vision')}>
80+
<div className={cx(styles.tag, styles.tagGreen)}>
81+
<Icon icon={LucideEye} />
82+
</div>
83+
</Tooltip>
84+
)}
85+
{model.functionCall && (
86+
<Tooltip
87+
overlayStyle={{ maxWidth: 'unset' }}
88+
placement={placement}
89+
title={t('ModelSelect.featureTag.functionCall')}
90+
>
91+
<div className={cx(styles.tag, styles.tagBlue)}>
92+
<Icon icon={ToyBrick} />
93+
</div>
94+
</Tooltip>
95+
)}
96+
{model.tokens && (
97+
<Tooltip
98+
overlayStyle={{ maxWidth: 'unset' }}
99+
placement={placement}
100+
title={t('ModelSelect.featureTag.tokens', {
101+
tokens: numeral(model.tokens).format('0,0'),
102+
})}
103+
>
104+
<Center className={styles.token}>{Math.floor(model.tokens / 1000)}K</Center>
105+
</Tooltip>
106+
)}
107+
{model.isCustom && (
108+
<Tooltip
109+
overlayStyle={{ maxWidth: 300 }}
110+
placement={placement}
111+
title={t('ModelSelect.featureTag.custom')}
112+
>
113+
<Center className={styles.custom}>DIY</Center>
114+
</Tooltip>
115+
)}
116+
</Flexbox>
117+
);
118+
},
119+
);
120+
59121
interface ModelItemRenderProps extends ChatModelCard {
60122
showInfoTag?: boolean;
61123
}
62124
export const ModelItemRender = memo<ModelItemRenderProps>(({ showInfoTag = true, ...model }) => {
63-
const { styles, cx } = useStyles();
64-
const { t } = useTranslation('common');
65-
66125
return (
67126
<Flexbox align={'center'} gap={32} horizontal justify={'space-between'}>
68127
<Flexbox align={'center'} gap={8} horizontal>
69128
<ModelIcon model={model.id} size={20} />
70129
{model.displayName || model.id}
71130
</Flexbox>
72131

73-
{showInfoTag && (
74-
<Flexbox gap={4} horizontal>
75-
{model.files && (
76-
<Tooltip placement={'right'} title={t('ModelSelect.featureTag.file')}>
77-
<div className={cx(styles.tag, styles.tagGreen)}>
78-
<Icon icon={LucidePaperclip} />
79-
</div>
80-
</Tooltip>
81-
)}
82-
{model.vision && (
83-
<Tooltip placement={'right'} title={t('ModelSelect.featureTag.vision')}>
84-
<div className={cx(styles.tag, styles.tagGreen)}>
85-
<Icon icon={LucideEye} />
86-
</div>
87-
</Tooltip>
88-
)}
89-
{model.functionCall && (
90-
<Tooltip
91-
overlayStyle={{ maxWidth: 'unset' }}
92-
placement={'right'}
93-
title={t('ModelSelect.featureTag.functionCall')}
94-
>
95-
<div className={cx(styles.tag, styles.tagBlue)}>
96-
<Icon icon={ToyBrick} />
97-
</div>
98-
</Tooltip>
99-
)}
100-
{model.tokens && (
101-
<Tooltip
102-
overlayStyle={{ maxWidth: 'unset' }}
103-
placement={'right'}
104-
title={t('ModelSelect.featureTag.tokens', {
105-
tokens: numeral(model.tokens).format('0,0'),
106-
})}
107-
>
108-
<Center className={styles.token}>{Math.floor(model.tokens / 1000)}K</Center>
109-
</Tooltip>
110-
)}
111-
{model.isCustom && (
112-
<Tooltip
113-
overlayStyle={{ maxWidth: 300 }}
114-
placement={'right'}
115-
title={t('ModelSelect.featureTag.custom')}
116-
>
117-
<Center className={styles.custom}>DIY</Center>
118-
</Tooltip>
119-
)}
120-
</Flexbox>
121-
)}
132+
{showInfoTag && <ModelInfoTags {...model} />}
122133
</Flexbox>
123134
);
124135
});

src/locales/default/setting.ts

+4
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ export default {
122122
title: 'API Key',
123123
},
124124
},
125+
modelList: {
126+
desc: '选择在会话中展示的模型,多个模型使用逗号(,) 隔开',
127+
title: '模型列表',
128+
},
125129
moonshot: {
126130
title: '月之暗面',
127131
token: {

0 commit comments

Comments
 (0)