Skip to content

Commit

Permalink
✨ feat: support model config modal
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Apr 10, 2024
1 parent ebfa0aa commit 62d6bb7
Show file tree
Hide file tree
Showing 14 changed files with 683 additions and 82 deletions.
5 changes: 0 additions & 5 deletions docs/package.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,29 +1,77 @@
import { Typography } from 'antd';
import isEqual from 'fast-deep-equal';
import { memo } from 'react';
import { ActionIcon } from '@lobehub/ui';
import { App, Typography } from 'antd';
import { LucideSettings, LucideTrash2 } from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

import ModelIcon from '@/components/ModelIcon';
import { ModelInfoTags } from '@/components/ModelSelect';
import { useGlobalStore } from '@/store/global';
import { modelProviderSelectors } from '@/store/global/selectors';
import { GlobalLLMProviderKey } from '@/types/settings';

const CustomModelOption = memo<{ displayName: string; id: string }>(({ displayName, id: id }) => {
const model = useGlobalStore((s) => modelProviderSelectors.modelCardById(id)(s), isEqual);
import ModelConfigModal from './ModelConfigModal';

interface CustomModelOptionProps {
id: string;
provider: GlobalLLMProviderKey;
}

const CustomModelOption = memo<CustomModelOptionProps>(({ id, provider }) => {
const { t } = useTranslation('common');
const { t: s } = useTranslation('setting');
const { modal } = App.useApp();

const [open, setOpen] = useState(true);
const [dispatchCustomModelCards] = useGlobalStore((s) => [s.dispatchCustomModelCards]);

return (
<Flexbox align={'center'} gap={8} horizontal>
<ModelIcon model={id} size={32} />
<Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
{displayName}
<ModelInfoTags directionReverse placement={'top'} {...model!} />
<>
<Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
<Flexbox>
<ModelIcon model={id} size={32} />
<Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
{id}
{/*<ModelInfoTags id={id} isCustom />*/}
</Flexbox>
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
{id}
</Typography.Text>
</Flexbox>
</Flexbox>

<Flexbox horizontal>
<ActionIcon
icon={LucideSettings}
onClick={async (e) => {
e.stopPropagation();
setOpen(true);
}}
title={s('llm.customModelCards.config')}
/>
<ActionIcon
icon={LucideTrash2}
onClick={async (e) => {
e.stopPropagation();
e.preventDefault();

const isConfirm = await modal.confirm({
centered: true,
content: s('llm.customModelCards.confirmDelete'),
okButtonProps: { danger: true },
type: 'warning',
});

if (isConfirm) {
dispatchCustomModelCards(provider, { id, type: 'delete' });
}
}}
title={t('delete')}
/>
</Flexbox>
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
{id}
</Typography.Text>
</Flexbox>
</Flexbox>
<ModelConfigModal onOpenChange={setOpen} open={open} provider={provider} />
</>
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Modal, SliderWithInput } from '@lobehub/ui';
import { Checkbox, Form, Input } from 'antd';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

import { GlobalLLMProviderKey } from '@/types/settings';

interface ModelConfigModalProps {
id: string;
onOpenChange: (open: boolean) => void;
open?: boolean;
provider: GlobalLLMProviderKey;
}
const ModelConfigModal = memo<ModelConfigModalProps>(({ open, id, onOpenChange }) => {
const [formInstance] = Form.useForm();
const { t } = useTranslation('setting');

return (
<Modal
maskClosable
onCancel={() => {
onOpenChange(false);
}}
open={open}
title={t('llm.customModelCards.modelConfig.modalTitle')}
>
<Form
colon={false}
form={formInstance}
labelCol={{ offset: 0, span: 4 }}
style={{ marginTop: 16 }}
wrapperCol={{ offset: 1, span: 19 }}
>
<Form.Item label={t('llm.customModelCards.modelConfig.id.title')} name={'id'}>
<Input placeholder={t('llm.customModelCards.modelConfig.id.placeholder')} />
</Form.Item>
<Form.Item
label={t('llm.customModelCards.modelConfig.displayName.title')}
name={'displayName'}
>
<Input placeholder={t('llm.customModelCards.modelConfig.displayName.placeholder')} />
</Form.Item>
<Form.Item label={t('llm.customModelCards.modelConfig.tokens.title')} name={'tokens'}>
<SliderWithInput
marks={{
100_000: '100k',
128_000: '128k',
16_385: '16k',
200_000: '200k',
32_768: '32k',
4096: '4k',
}}
max={200_000}
min={0}
/>
</Form.Item>
<Form.Item
extra={t('llm.customModelCards.modelConfig.functionCall.extra')}
label={t('llm.customModelCards.modelConfig.functionCall.title')}
name={'functionCall'}
>
<Checkbox />
</Form.Item>
<Form.Item
extra={t('llm.customModelCards.modelConfig.vision.extra')}
label={t('llm.customModelCards.modelConfig.vision.title')}
name={'vision'}
>
<Checkbox />
</Form.Item>
<Form.Item
extra={t('llm.customModelCards.modelConfig.files.extra')}
label={t('llm.customModelCards.modelConfig.files.title')}
name={'files'}
>
<Checkbox />
</Form.Item>
</Form>
</Modal>
);
});
export default ModelConfigModal;
38 changes: 23 additions & 15 deletions src/app/settings/llm/components/ProviderModelList/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,32 @@ import ModelIcon from '@/components/ModelIcon';
import { ModelInfoTags } from '@/components/ModelSelect';
import { useGlobalStore } from '@/store/global';
import { modelProviderSelectors } from '@/store/global/selectors';
import { GlobalLLMProviderKey } from '@/types/settings';

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

return (
<Flexbox align={'center'} gap={8} horizontal>
<ModelIcon model={id} size={32} />
<Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
{displayName}
<ModelInfoTags directionReverse placement={'top'} {...model!} />
const OptionRender = memo<{ displayName: string; id: string; provider: GlobalLLMProviderKey }>(
({ displayName, id, provider }) => {
const model = useGlobalStore((s) => modelProviderSelectors.modelCardById(id)(s), isEqual);

// if there is no model, it means it is a user custom model
if (!model) return <CustomModelOption id={id} provider={provider} />;

return (
<Flexbox align={'center'} gap={8} horizontal>
<ModelIcon model={id} size={32} />
<Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
{displayName}
<ModelInfoTags directionReverse placement={'top'} {...model!} />
</Flexbox>
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
{id}
</Typography.Text>
</Flexbox>
<Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
{id}
</Typography.Text>
</Flexbox>
</Flexbox>
);
});
);
},
);

export default OptionRender;
127 changes: 88 additions & 39 deletions src/app/settings/llm/components/ProviderModelList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,122 @@
import { ActionIcon } from '@lobehub/ui';
import { Select } from 'antd';
import { css, cx } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { RotateCwIcon } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

import { filterEnabledModels } from '@/config/modelProviders';
import { useGlobalStore } from '@/store/global';
import { modelConfigSelectors, modelProviderSelectors } from '@/store/global/selectors';
import { GlobalLLMProviderKey } from '@/types/settings';

import CustomModelOption from './CustomModelOption';
import OptionRender from './Option';

const popup = css`
&.ant-select-dropdown {
.ant-select-item-option-selected {
font-weight: normal;
const styles = {
popup: css`
&.ant-select-dropdown {
.ant-select-item-option-selected {
font-weight: normal;
}
}
}
`;
`,
reset: css`
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 20;
inset-inline-end: 28px;
`,
};

interface CustomModelSelectProps {
onChange?: (value: string[]) => void;
placeholder?: string;
provider: string;
value?: string[];
provider: GlobalLLMProviderKey;
}

const ProviderModelListSelect = memo<CustomModelSelectProps>(
({ provider, placeholder, onChange }) => {
const providerCard = useGlobalStore(
(s) => modelProviderSelectors.providerModelList(s).find((s) => s.id === provider),
isEqual,
);
const providerConfig = useGlobalStore((s) =>
modelConfigSelectors.providerConfig(provider as GlobalLLMProviderKey)(s),
);
const ProviderModelListSelect = memo<CustomModelSelectProps>(({ provider, placeholder }) => {
const { t } = useTranslation('common');
const { t: transSetting } = useTranslation('setting');
const chatModelCards = useGlobalStore(modelConfigSelectors.providerModelCards(provider), isEqual);
const [setModelProviderConfig, dispatchCustomModelCards] = useGlobalStore((s) => [
s.setModelProviderConfig,
s.dispatchCustomModelCards,
]);
const defaultEnableModel = useGlobalStore(
modelProviderSelectors.defaultEnabledProviderModels(provider),
isEqual,
);
const enabledModels = useGlobalStore(
modelConfigSelectors.providerEnableModels(provider),
isEqual,
);
const showReset = !!enabledModels && !isEqual(defaultEnableModel, enabledModels);

const defaultEnableModel = providerCard ? filterEnabledModels(providerCard) : [];

const chatModels = providerCard?.chatModels || [];

return (
return (
<div style={{ position: 'relative' }}>
<div className={cx(styles.reset)}>
{showReset && (
<ActionIcon
icon={RotateCwIcon}
onClick={() => {
setModelProviderConfig(provider, { enabledModels: null });
}}
size={'small'}
title={t('reset')}
/>
)}
</div>
<Select<string[]>
allowClear
defaultValue={defaultEnableModel}
mode="tags"
onChange={(value) => {
onChange?.(value.filter(Boolean));
onChange={(value, options) => {
setModelProviderConfig(provider, { enabledModels: value.filter(Boolean) });

// if there is a new model, add it to `customModelCards`
options.forEach((option: { label?: string; value?: string }, index: number) => {
// if is a known model, it should have value
// if is an unknown model, the option will be {}
if (option.value) return;

const modelId = value[index];

dispatchCustomModelCards(provider, {
modelCard: { id: modelId },
type: 'add',
});
});
}}
open
optionFilterProp="label"
optionRender={({ label, value }) => {
console.log(value);
// model is in the chatModels
if (chatModels.some((c) => c.id === value))
return <OptionRender displayName={label as string} id={value as string} />;
if (chatModelCards.some((c) => c.id === value))
return (
<OptionRender
displayName={label as string}
id={value as string}
provider={provider}
/>
);

// model is user defined in client
return <CustomModelOption displayName={label as string} id={value as string} />;
// model is defined by user in client
return (
<Flexbox align={'center'} gap={8} horizontal>
{transSetting('llm.customModelCards.addNew', { id: value })}
</Flexbox>
);
}}
options={chatModels.map((model) => ({
options={chatModelCards.map((model) => ({
label: model.displayName || model.id,
value: model.id,
}))}
placeholder={placeholder}
popupClassName={cx(popup)}
value={providerConfig?.enabledModels.filter(Boolean)}
popupClassName={cx(styles.popup)}
value={enabledModels ?? defaultEnableModel}
/>
);
},
);
</div>
);
});

export default ProviderModelListSelect;
Loading

0 comments on commit 62d6bb7

Please sign in to comment.