Skip to content

Commit 62d6bb7

Browse files
committed
✨ feat: support model config modal
1 parent ebfa0aa commit 62d6bb7

File tree

14 files changed

+683
-82
lines changed

14 files changed

+683
-82
lines changed

docs/package.json

-5
This file was deleted.

src/app/settings/llm/components/ProviderModelList/CustomModelOption.tsx

+65-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,77 @@
1-
import { Typography } from 'antd';
2-
import isEqual from 'fast-deep-equal';
3-
import { memo } from 'react';
1+
import { ActionIcon } from '@lobehub/ui';
2+
import { App, Typography } from 'antd';
3+
import { LucideSettings, LucideTrash2 } from 'lucide-react';
4+
import { memo, useState } from 'react';
5+
import { useTranslation } from 'react-i18next';
46
import { Flexbox } from 'react-layout-kit';
57

68
import ModelIcon from '@/components/ModelIcon';
7-
import { ModelInfoTags } from '@/components/ModelSelect';
89
import { useGlobalStore } from '@/store/global';
9-
import { modelProviderSelectors } from '@/store/global/selectors';
10+
import { GlobalLLMProviderKey } from '@/types/settings';
1011

11-
const CustomModelOption = memo<{ displayName: string; id: string }>(({ displayName, id: id }) => {
12-
const model = useGlobalStore((s) => modelProviderSelectors.modelCardById(id)(s), isEqual);
12+
import ModelConfigModal from './ModelConfigModal';
13+
14+
interface CustomModelOptionProps {
15+
id: string;
16+
provider: GlobalLLMProviderKey;
17+
}
18+
19+
const CustomModelOption = memo<CustomModelOptionProps>(({ id, provider }) => {
20+
const { t } = useTranslation('common');
21+
const { t: s } = useTranslation('setting');
22+
const { modal } = App.useApp();
23+
24+
const [open, setOpen] = useState(true);
25+
const [dispatchCustomModelCards] = useGlobalStore((s) => [s.dispatchCustomModelCards]);
1326

1427
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!} />
28+
<>
29+
<Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
30+
<Flexbox>
31+
<ModelIcon model={id} size={32} />
32+
<Flexbox>
33+
<Flexbox align={'center'} gap={8} horizontal>
34+
{id}
35+
{/*<ModelInfoTags id={id} isCustom />*/}
36+
</Flexbox>
37+
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
38+
{id}
39+
</Typography.Text>
40+
</Flexbox>
41+
</Flexbox>
42+
43+
<Flexbox horizontal>
44+
<ActionIcon
45+
icon={LucideSettings}
46+
onClick={async (e) => {
47+
e.stopPropagation();
48+
setOpen(true);
49+
}}
50+
title={s('llm.customModelCards.config')}
51+
/>
52+
<ActionIcon
53+
icon={LucideTrash2}
54+
onClick={async (e) => {
55+
e.stopPropagation();
56+
e.preventDefault();
57+
58+
const isConfirm = await modal.confirm({
59+
centered: true,
60+
content: s('llm.customModelCards.confirmDelete'),
61+
okButtonProps: { danger: true },
62+
type: 'warning',
63+
});
64+
65+
if (isConfirm) {
66+
dispatchCustomModelCards(provider, { id, type: 'delete' });
67+
}
68+
}}
69+
title={t('delete')}
70+
/>
2171
</Flexbox>
22-
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
23-
{id}
24-
</Typography.Text>
2572
</Flexbox>
26-
</Flexbox>
73+
<ModelConfigModal onOpenChange={setOpen} open={open} provider={provider} />
74+
</>
2775
);
2876
});
2977

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Modal, SliderWithInput } from '@lobehub/ui';
2+
import { Checkbox, Form, Input } from 'antd';
3+
import { memo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
6+
import { GlobalLLMProviderKey } from '@/types/settings';
7+
8+
interface ModelConfigModalProps {
9+
id: string;
10+
onOpenChange: (open: boolean) => void;
11+
open?: boolean;
12+
provider: GlobalLLMProviderKey;
13+
}
14+
const ModelConfigModal = memo<ModelConfigModalProps>(({ open, id, onOpenChange }) => {
15+
const [formInstance] = Form.useForm();
16+
const { t } = useTranslation('setting');
17+
18+
return (
19+
<Modal
20+
maskClosable
21+
onCancel={() => {
22+
onOpenChange(false);
23+
}}
24+
open={open}
25+
title={t('llm.customModelCards.modelConfig.modalTitle')}
26+
>
27+
<Form
28+
colon={false}
29+
form={formInstance}
30+
labelCol={{ offset: 0, span: 4 }}
31+
style={{ marginTop: 16 }}
32+
wrapperCol={{ offset: 1, span: 19 }}
33+
>
34+
<Form.Item label={t('llm.customModelCards.modelConfig.id.title')} name={'id'}>
35+
<Input placeholder={t('llm.customModelCards.modelConfig.id.placeholder')} />
36+
</Form.Item>
37+
<Form.Item
38+
label={t('llm.customModelCards.modelConfig.displayName.title')}
39+
name={'displayName'}
40+
>
41+
<Input placeholder={t('llm.customModelCards.modelConfig.displayName.placeholder')} />
42+
</Form.Item>
43+
<Form.Item label={t('llm.customModelCards.modelConfig.tokens.title')} name={'tokens'}>
44+
<SliderWithInput
45+
marks={{
46+
100_000: '100k',
47+
128_000: '128k',
48+
16_385: '16k',
49+
200_000: '200k',
50+
32_768: '32k',
51+
4096: '4k',
52+
}}
53+
max={200_000}
54+
min={0}
55+
/>
56+
</Form.Item>
57+
<Form.Item
58+
extra={t('llm.customModelCards.modelConfig.functionCall.extra')}
59+
label={t('llm.customModelCards.modelConfig.functionCall.title')}
60+
name={'functionCall'}
61+
>
62+
<Checkbox />
63+
</Form.Item>
64+
<Form.Item
65+
extra={t('llm.customModelCards.modelConfig.vision.extra')}
66+
label={t('llm.customModelCards.modelConfig.vision.title')}
67+
name={'vision'}
68+
>
69+
<Checkbox />
70+
</Form.Item>
71+
<Form.Item
72+
extra={t('llm.customModelCards.modelConfig.files.extra')}
73+
label={t('llm.customModelCards.modelConfig.files.title')}
74+
name={'files'}
75+
>
76+
<Checkbox />
77+
</Form.Item>
78+
</Form>
79+
</Modal>
80+
);
81+
});
82+
export default ModelConfigModal;

src/app/settings/llm/components/ProviderModelList/Option.tsx

+23-15
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,32 @@ import ModelIcon from '@/components/ModelIcon';
77
import { ModelInfoTags } from '@/components/ModelSelect';
88
import { useGlobalStore } from '@/store/global';
99
import { modelProviderSelectors } from '@/store/global/selectors';
10+
import { GlobalLLMProviderKey } from '@/types/settings';
1011

11-
const OptionRender = memo<{ displayName: string; id: string }>(({ displayName, id: id }) => {
12-
const model = useGlobalStore((s) => modelProviderSelectors.modelCardById(id)(s), isEqual);
12+
import CustomModelOption from './CustomModelOption';
1313

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!} />
14+
const OptionRender = memo<{ displayName: string; id: string; provider: GlobalLLMProviderKey }>(
15+
({ displayName, id, provider }) => {
16+
const model = useGlobalStore((s) => modelProviderSelectors.modelCardById(id)(s), isEqual);
17+
18+
// if there is no model, it means it is a user custom model
19+
if (!model) return <CustomModelOption id={id} provider={provider} />;
20+
21+
return (
22+
<Flexbox align={'center'} gap={8} horizontal>
23+
<ModelIcon model={id} size={32} />
24+
<Flexbox>
25+
<Flexbox align={'center'} gap={8} horizontal>
26+
{displayName}
27+
<ModelInfoTags directionReverse placement={'top'} {...model!} />
28+
</Flexbox>
29+
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
30+
{id}
31+
</Typography.Text>
2132
</Flexbox>
22-
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
23-
{id}
24-
</Typography.Text>
2533
</Flexbox>
26-
</Flexbox>
27-
);
28-
});
34+
);
35+
},
36+
);
2937

3038
export default OptionRender;
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,122 @@
1+
import { ActionIcon } from '@lobehub/ui';
12
import { Select } from 'antd';
23
import { css, cx } from 'antd-style';
34
import isEqual from 'fast-deep-equal';
5+
import { RotateCwIcon } from 'lucide-react';
46
import { memo } from 'react';
7+
import { useTranslation } from 'react-i18next';
8+
import { Flexbox } from 'react-layout-kit';
59

6-
import { filterEnabledModels } from '@/config/modelProviders';
710
import { useGlobalStore } from '@/store/global';
811
import { modelConfigSelectors, modelProviderSelectors } from '@/store/global/selectors';
912
import { GlobalLLMProviderKey } from '@/types/settings';
1013

11-
import CustomModelOption from './CustomModelOption';
1214
import OptionRender from './Option';
1315

14-
const popup = css`
15-
&.ant-select-dropdown {
16-
.ant-select-item-option-selected {
17-
font-weight: normal;
16+
const styles = {
17+
popup: css`
18+
&.ant-select-dropdown {
19+
.ant-select-item-option-selected {
20+
font-weight: normal;
21+
}
1822
}
19-
}
20-
`;
23+
`,
24+
reset: css`
25+
position: absolute;
26+
top: 50%;
27+
transform: translateY(-50%);
28+
z-index: 20;
29+
inset-inline-end: 28px;
30+
`,
31+
};
2132

2233
interface CustomModelSelectProps {
23-
onChange?: (value: string[]) => void;
2434
placeholder?: string;
25-
provider: string;
26-
value?: string[];
35+
provider: GlobalLLMProviderKey;
2736
}
2837

29-
const ProviderModelListSelect = memo<CustomModelSelectProps>(
30-
({ provider, placeholder, onChange }) => {
31-
const providerCard = useGlobalStore(
32-
(s) => modelProviderSelectors.providerModelList(s).find((s) => s.id === provider),
33-
isEqual,
34-
);
35-
const providerConfig = useGlobalStore((s) =>
36-
modelConfigSelectors.providerConfig(provider as GlobalLLMProviderKey)(s),
37-
);
38+
const ProviderModelListSelect = memo<CustomModelSelectProps>(({ provider, placeholder }) => {
39+
const { t } = useTranslation('common');
40+
const { t: transSetting } = useTranslation('setting');
41+
const chatModelCards = useGlobalStore(modelConfigSelectors.providerModelCards(provider), isEqual);
42+
const [setModelProviderConfig, dispatchCustomModelCards] = useGlobalStore((s) => [
43+
s.setModelProviderConfig,
44+
s.dispatchCustomModelCards,
45+
]);
46+
const defaultEnableModel = useGlobalStore(
47+
modelProviderSelectors.defaultEnabledProviderModels(provider),
48+
isEqual,
49+
);
50+
const enabledModels = useGlobalStore(
51+
modelConfigSelectors.providerEnableModels(provider),
52+
isEqual,
53+
);
54+
const showReset = !!enabledModels && !isEqual(defaultEnableModel, enabledModels);
3855

39-
const defaultEnableModel = providerCard ? filterEnabledModels(providerCard) : [];
40-
41-
const chatModels = providerCard?.chatModels || [];
42-
43-
return (
56+
return (
57+
<div style={{ position: 'relative' }}>
58+
<div className={cx(styles.reset)}>
59+
{showReset && (
60+
<ActionIcon
61+
icon={RotateCwIcon}
62+
onClick={() => {
63+
setModelProviderConfig(provider, { enabledModels: null });
64+
}}
65+
size={'small'}
66+
title={t('reset')}
67+
/>
68+
)}
69+
</div>
4470
<Select<string[]>
4571
allowClear
46-
defaultValue={defaultEnableModel}
4772
mode="tags"
48-
onChange={(value) => {
49-
onChange?.(value.filter(Boolean));
73+
onChange={(value, options) => {
74+
setModelProviderConfig(provider, { enabledModels: value.filter(Boolean) });
75+
76+
// if there is a new model, add it to `customModelCards`
77+
options.forEach((option: { label?: string; value?: string }, index: number) => {
78+
// if is a known model, it should have value
79+
// if is an unknown model, the option will be {}
80+
if (option.value) return;
81+
82+
const modelId = value[index];
83+
84+
dispatchCustomModelCards(provider, {
85+
modelCard: { id: modelId },
86+
type: 'add',
87+
});
88+
});
5089
}}
90+
open
5191
optionFilterProp="label"
5292
optionRender={({ label, value }) => {
53-
console.log(value);
5493
// model is in the chatModels
55-
if (chatModels.some((c) => c.id === value))
56-
return <OptionRender displayName={label as string} id={value as string} />;
94+
if (chatModelCards.some((c) => c.id === value))
95+
return (
96+
<OptionRender
97+
displayName={label as string}
98+
id={value as string}
99+
provider={provider}
100+
/>
101+
);
57102

58-
// model is user defined in client
59-
return <CustomModelOption displayName={label as string} id={value as string} />;
103+
// model is defined by user in client
104+
return (
105+
<Flexbox align={'center'} gap={8} horizontal>
106+
{transSetting('llm.customModelCards.addNew', { id: value })}
107+
</Flexbox>
108+
);
60109
}}
61-
options={chatModels.map((model) => ({
110+
options={chatModelCards.map((model) => ({
62111
label: model.displayName || model.id,
63112
value: model.id,
64113
}))}
65114
placeholder={placeholder}
66-
popupClassName={cx(popup)}
67-
value={providerConfig?.enabledModels.filter(Boolean)}
115+
popupClassName={cx(styles.popup)}
116+
value={enabledModels ?? defaultEnableModel}
68117
/>
69-
);
70-
},
71-
);
118+
</div>
119+
);
120+
});
72121

73122
export default ProviderModelListSelect;

0 commit comments

Comments
 (0)