Feat: Add model editing functionality with improved UI labels (#8855)

### What problem does this PR solve?

Add edit button for local LLM models
<img width="1531" height="1428" alt="image"
src="https://github.com/user-attachments/assets/19d62255-59a6-4a7e-9772-8b8743101f78"
/>

<img width="1531" height="1428" alt="image"
src="https://github.com/user-attachments/assets/c3a0f77e-cc6b-4190-95a6-13835463428b"
/>



### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

---------

Co-authored-by: Liu An <asiro@qq.com>
This commit is contained in:
Adrian Altermatt
2025-07-21 13:16:53 +02:00
committed by GitHub
parent dbc267758e
commit 6691532079
15 changed files with 197 additions and 30 deletions

View File

@ -9,6 +9,7 @@ interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
initialValue: string;
llmFactory: string;
editMode?: boolean;
onOk: (postBody: ApiKeyPostBody) => void;
showModal?(): void;
}
@ -27,6 +28,7 @@ const ApiKeyModal = ({
llmFactory,
loading,
initialValue,
editMode = false,
onOk,
}: IProps) => {
const [form] = Form.useForm();
@ -52,7 +54,7 @@ const ApiKeyModal = ({
return (
<Modal
title={t('modify')}
title={editMode ? t('editModel') : t('modify')}
open={visible}
onOk={handleOk}
onCancel={hideModal}

View File

@ -11,6 +11,7 @@ import {
} from '@/hooks/llm-hooks';
import { useFetchTenantInfo } from '@/hooks/user-setting-hooks';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { getRealModelName } from '@/utils/llm-util';
import { useCallback, useState } from 'react';
import { ApiKeyPostBody } from '../interface';
@ -20,6 +21,7 @@ export const useSubmitApiKey = () => {
const [savingParams, setSavingParams] = useState<SavingParamsState>(
{} as SavingParamsState,
);
const [editMode, setEditMode] = useState(false);
const { saveApiKey, loading } = useSaveApiKey();
const {
visible: apiKeyVisible,
@ -36,14 +38,16 @@ export const useSubmitApiKey = () => {
if (ret === 0) {
hideApiKeyModal();
setEditMode(false);
}
},
[hideApiKeyModal, saveApiKey, savingParams],
);
const onShowApiKeyModal = useCallback(
(savingParams: SavingParamsState) => {
(savingParams: SavingParamsState, isEdit = false) => {
setSavingParams(savingParams);
setEditMode(isEdit);
showApiKeyModal();
},
[showApiKeyModal, setSavingParams],
@ -53,6 +57,7 @@ export const useSubmitApiKey = () => {
saveApiKeyLoading: loading,
initialApiKey: '',
llmFactory: savingParams.llm_factory,
editMode,
onApiKeySavingOk,
apiKeyVisible,
hideApiKeyModal,
@ -105,6 +110,9 @@ export const useFetchSystemModelSettingOnMount = () => {
export const useSubmitOllama = () => {
const [selectedLlmFactory, setSelectedLlmFactory] = useState<string>('');
const [editMode, setEditMode] = useState(false);
const [initialValues, setInitialValues] = useState<Partial<IAddLlmRequestBody> | undefined>();
const [originalModelName, setOriginalModelName] = useState<string>('');
const { addLlm, loading } = useAddLlm();
const {
visible: llmAddingVisible,
@ -114,21 +122,44 @@ export const useSubmitOllama = () => {
const onLlmAddingOk = useCallback(
async (payload: IAddLlmRequestBody) => {
const ret = await addLlm(payload);
const cleanedPayload = { ...payload };
if (!cleanedPayload.api_key || cleanedPayload.api_key.trim() === '') {
delete cleanedPayload.api_key;
}
const ret = await addLlm(cleanedPayload);
if (ret === 0) {
hideLlmAddingModal();
setEditMode(false);
setInitialValues(undefined);
}
},
[hideLlmAddingModal, addLlm],
);
const handleShowLlmAddingModal = (llmFactory: string) => {
const handleShowLlmAddingModal = (llmFactory: string, isEdit = false, modelData?: any, detailedData?: any) => {
setSelectedLlmFactory(llmFactory);
setEditMode(isEdit);
if (isEdit && detailedData) {
const initialVals = {
llm_name: getRealModelName(detailedData.name),
model_type: detailedData.type,
api_base: detailedData.api_base || '',
max_tokens: detailedData.max_tokens || 8192,
api_key: '',
};
setInitialValues(initialVals);
} else {
setInitialValues(undefined);
}
showLlmAddingModal();
};
return {
llmAddingLoading: loading,
editMode,
initialValues,
onLlmAddingOk,
llmAddingVisible,
hideLlmAddingModal,

View File

@ -3,9 +3,9 @@ import { LlmIcon } from '@/components/svg-icon';
import { useTheme } from '@/components/theme-provider';
import { LLMFactory } from '@/constants/llm';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { LlmItem, useSelectLlmList } from '@/hooks/llm-hooks';
import { LlmItem, useSelectLlmList, useFetchMyLlmListDetailed } from '@/hooks/llm-hooks';
import { getRealModelName } from '@/utils/llm-util';
import { CloseCircleOutlined, SettingOutlined } from '@ant-design/icons';
import { CloseCircleOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons';
import {
Button,
Card,
@ -60,9 +60,10 @@ const { Text } = Typography;
interface IModelCardProps {
item: LlmItem;
clickApiKey: (llmFactory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}
const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
const ModelCard = ({ item, clickApiKey, handleEditModel }: IModelCardProps) => {
const { visible, switchVisible } = useSetModalState();
const { t } = useTranslate('setting');
const { theme } = useTheme();
@ -112,7 +113,7 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
</Button>
<Button onClick={handleShowMoreClick}>
<Flex align="center" gap={4}>
{t('showMoreModels')}
{visible ? t('hideModels') : t('showMoreModels')}
<MoreModelIcon />
</Flex>
</Button>
@ -129,13 +130,20 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
size="small"
dataSource={item.llm}
className={styles.llmList}
renderItem={(item) => (
renderItem={(model) => (
<List.Item>
<Space>
{getRealModelName(item.name)}
<Tag color="#b8b8b8">{item.type}</Tag>
{getRealModelName(model.name)}
<Tag color="#b8b8b8">{model.type}</Tag>
{isLocalLlmFactory(item.name) && (
<Tooltip title={t('edit', { keyPrefix: 'common' })}>
<Button type={'text'} onClick={() => handleEditModel(model, item)}>
<EditOutlined style={{ color: '#1890ff' }} />
</Button>
</Tooltip>
)}
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button type={'text'} onClick={handleDeleteLlm(item.name)}>
<Button type={'text'} onClick={handleDeleteLlm(model.name)}>
<CloseCircleOutlined style={{ color: '#D92D20' }} />
</Button>
</Tooltip>
@ -151,11 +159,13 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
const UserSettingModel = () => {
const { factoryList, myLlmList: llmList, loading } = useSelectLlmList();
const { data: detailedLlmList } = useFetchMyLlmListDetailed();
const { theme } = useTheme();
const {
saveApiKeyLoading,
initialApiKey,
llmFactory,
editMode,
onApiKeySavingOk,
apiKeyVisible,
hideApiKeyModal,
@ -175,6 +185,8 @@ const UserSettingModel = () => {
showLlmAddingModal,
onLlmAddingOk,
llmAddingLoading,
editMode: llmEditMode,
initialValues: llmInitialValues,
selectedLlmFactory,
} = useSubmitOllama();
@ -288,6 +300,30 @@ const UserSettingModel = () => {
[showApiKeyModal, showLlmAddingModal, ModalMap],
);
const handleEditModel = useCallback(
(model: any, factory: LlmItem) => {
if (factory) {
const detailedFactory = detailedLlmList[factory.name];
const detailedModel = detailedFactory?.llm?.find((m: any) => m.name === model.name);
const editData = {
llm_factory: factory.name,
llm_name: model.name,
model_type: model.type
};
if (isLocalLlmFactory(factory.name)) {
showLlmAddingModal(factory.name, true, editData, detailedModel);
} else if (factory.name in ModalMap) {
ModalMap[factory.name as keyof typeof ModalMap]();
} else {
showApiKeyModal(editData, true);
}
}
},
[showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
);
const items: CollapseProps['items'] = [
{
key: '1',
@ -297,7 +333,7 @@ const UserSettingModel = () => {
grid={{ gutter: 16, column: 1 }}
dataSource={llmList}
renderItem={(item) => (
<ModelCard item={item} clickApiKey={handleAddModel}></ModelCard>
<ModelCard item={item} clickApiKey={handleAddModel} handleEditModel={handleEditModel}></ModelCard>
)}
/>
),
@ -384,6 +420,7 @@ const UserSettingModel = () => {
hideModal={hideApiKeyModal}
loading={saveApiKeyLoading}
initialValue={initialApiKey}
editMode={editMode}
onOk={onApiKeySavingOk}
llmFactory={llmFactory}
></ApiKeyModal>
@ -400,6 +437,8 @@ const UserSettingModel = () => {
hideModal={hideLlmAddingModal}
onOk={onLlmAddingOk}
loading={llmAddingLoading}
editMode={llmEditMode}
initialValues={llmInitialValues}
llmFactory={selectedLlmFactory}
></OllamaModal>
<VolcEngineModal

View File

@ -13,6 +13,7 @@ import {
Switch,
} from 'antd';
import omit from 'lodash/omit';
import { useEffect } from 'react';
type FieldType = IAddLlmRequestBody & { vision: boolean };
@ -45,7 +46,13 @@ const OllamaModal = ({
onOk,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & { llmFactory: string }) => {
editMode = false,
initialValues,
}: IModalProps<IAddLlmRequestBody> & {
llmFactory: string;
editMode?: boolean;
initialValues?: Partial<IAddLlmRequestBody>;
}) => {
const [form] = Form.useForm<FieldType>();
const { t } = useTranslate('setting');
@ -73,6 +80,22 @@ const OllamaModal = ({
await handleOk();
}
};
useEffect(() => {
if (visible && editMode && initialValues) {
const formValues = {
llm_name: initialValues.llm_name,
model_type: initialValues.model_type,
api_base: initialValues.api_base,
max_tokens: initialValues.max_tokens || 8192,
api_key: '',
...initialValues,
};
form.setFieldsValue(formValues);
} else if (visible && !editMode) {
form.resetFields();
}
}, [visible, editMode, initialValues, form]);
const url =
llmFactoryToUrlMap[llmFactory as LlmFactory] ||
@ -111,7 +134,7 @@ const OllamaModal = ({
};
return (
<Modal
title={t('addLlmTitle', { name: llmFactory })}
title={editMode ? t('editLlmTitle', { name: llmFactory }) : t('addLlmTitle', { name: llmFactory })}
open={visible}
onOk={handleOk}
onCancel={hideModal}
@ -173,7 +196,10 @@ const OllamaModal = ({
name="api_key"
rules={[{ required: false, message: t('apiKeyMessage') }]}
>
<Input placeholder={t('apiKeyMessage')} onKeyDown={handleKeyDown} />
<Input
placeholder={t('apiKeyMessage')}
onKeyDown={handleKeyDown}
/>
</Form.Item>
<Form.Item<FieldType>
label={t('maxTokens')}