From 2ca0dc0fc5ebd9fd29947a46caf640d02f9d94b0 Mon Sep 17 00:00:00 2001 From: balibabu <cike8899@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:58:09 +0800 Subject: [PATCH] feat: submit api key and add language to Configuration and fetch llm factory list on UserSettingModel mount (#121) * feat: fetch llm factory list on UserSettingModel mount * feat: add language to Configuration * feat: submit api key --- web/src/hooks/llmHooks.ts | 99 +++++++++++++- web/src/interfaces/database/llm.ts | 22 +++ .../knowledge-setting/configuration.tsx | 36 +++-- web/src/pages/setting/model.ts | 37 +++-- .../setting-model/api-key-modal/index.tsx | 78 +++++++++++ .../pages/user-setting/setting-model/hooks.ts | 50 +++++++ .../user-setting/setting-model/index.less | 6 + .../user-setting/setting-model/index.tsx | 129 +++++++++++++++++- 8 files changed, 422 insertions(+), 35 deletions(-) create mode 100644 web/src/pages/user-setting/setting-model/api-key-modal/index.tsx create mode 100644 web/src/pages/user-setting/setting-model/hooks.ts create mode 100644 web/src/pages/user-setting/setting-model/index.less diff --git a/web/src/hooks/llmHooks.ts b/web/src/hooks/llmHooks.ts index 7800074..eed420c 100644 --- a/web/src/hooks/llmHooks.ts +++ b/web/src/hooks/llmHooks.ts @@ -1,5 +1,9 @@ import { LlmModelType } from '@/constants/knowledge'; -import { IThirdOAIModelCollection } from '@/interfaces/database/llm'; +import { + IFactory, + IMyLlmValue, + IThirdOAIModelCollection, +} from '@/interfaces/database/llm'; import { useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'umi'; @@ -38,3 +42,96 @@ export const useSelectLlmOptions = () => { return embeddingModelOptions; }; + +export const useSelectLlmFactoryList = () => { + const factoryList: IFactory[] = useSelector( + (state: any) => state.settingModel.factoryList, + ); + + return factoryList; +}; + +export const useSelectMyLlmList = () => { + const myLlmList: Record<string, IMyLlmValue> = useSelector( + (state: any) => state.settingModel.myLlmList, + ); + + return myLlmList; +}; + +export const useFetchLlmFactoryListOnMount = () => { + const dispatch = useDispatch(); + const factoryList = useSelectLlmFactoryList(); + const myLlmList = useSelectMyLlmList(); + + const list = useMemo( + () => + factoryList.filter((x) => + Object.keys(myLlmList).every((y) => y !== x.name), + ), + [factoryList, myLlmList], + ); + + const fetchLlmFactoryList = useCallback(() => { + dispatch({ + type: 'settingModel/factories_list', + }); + }, [dispatch]); + + useEffect(() => { + fetchLlmFactoryList(); + }, [fetchLlmFactoryList]); + + return list; +}; + +export const useFetchMyLlmListOnMount = () => { + const dispatch = useDispatch(); + const llmList = useSelectMyLlmList(); + const factoryList = useSelectLlmFactoryList(); + + const list: Array<{ name: string; logo: string } & IMyLlmValue> = + useMemo(() => { + return Object.entries(llmList).map(([key, value]) => ({ + name: key, + logo: factoryList.find((x) => x.name === key)?.logo ?? '', + ...value, + })); + }, [llmList, factoryList]); + + const fetchMyLlmList = useCallback(() => { + dispatch({ + type: 'settingModel/my_llm', + }); + }, [dispatch]); + + useEffect(() => { + fetchMyLlmList(); + }, [fetchMyLlmList]); + + return list; +}; + +export interface IApiKeySavingParams { + llm_factory: string; + api_key: string; + llm_name?: string; + model_type?: string; + api_base?: string; +} + +export const useSaveApiKey = () => { + const dispatch = useDispatch(); + + const saveApiKey = useCallback( + (savingParams: IApiKeySavingParams) => { + return dispatch<any>({ + type: 'settingModel/set_api_key', + payload: savingParams, + }); + }, + [dispatch], + ); + + return saveApiKey; +}; diff --git a/web/src/interfaces/database/llm.ts b/web/src/interfaces/database/llm.ts index 6e8c7a9..ff99613 100644 --- a/web/src/interfaces/database/llm.ts +++ b/web/src/interfaces/database/llm.ts @@ -14,3 +14,25 @@ export interface IThirdOAIModel { } export type IThirdOAIModelCollection = Record<string, IThirdOAIModel[]>; + +export interface IFactory { + create_date: string; + create_time: number; + logo: string; + name: string; + status: string; + tags: string; + update_date: string; + update_time: number; +} + +export interface IMyLlmValue { + llm: Llm[]; + tags: string; +} + +export interface Llm { + name: string; + type: string; + used_token: number; +} diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx index b06eda3..e57e7ea 100644 --- a/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx @@ -2,11 +2,19 @@ import { useFetchKnowledgeBaseConfiguration, useKnowledgeBaseId, } from '@/hooks/knowledgeHook'; +import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks'; +import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { useFetchTenantInfo, useSelectParserList, } from '@/hooks/userSettingHook'; - +import { IKnowledge } from '@/interfaces/database/knowledge'; +import { + getBase64FromUploadFileList, + getUploadFileListFromBase64, + normFile, +} from '@/utils/fileUtil'; +import { PlusOutlined } from '@ant-design/icons'; import { Button, Divider, @@ -25,17 +33,8 @@ import { import pick from 'lodash/pick'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; - -import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks'; -import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; -import { IKnowledge } from '@/interfaces/database/knowledge'; -import { - getBase64FromUploadFileList, - getUploadFileListFromBase64, - normFile, -} from '@/utils/fileUtil'; -import { PlusOutlined } from '@ant-design/icons'; import { LlmModelType } from '../../constant'; + import styles from './index.less'; const { Title } = Typography; @@ -83,6 +82,7 @@ const Configuration = () => { 'permission', 'embd_id', 'parser_id', + 'language', 'parser_config.chunk_token_num', ]), avatar: fileList, @@ -131,9 +131,20 @@ const Configuration = () => { </button> </Upload> </Form.Item> - <Form.Item name="description" label="Knowledge base bio"> + <Form.Item name="description" label="Description"> <Input /> </Form.Item> + <Form.Item + label="Language" + name="language" + initialValue={'Chinese'} + rules={[{ required: true, message: 'Please input your language!' }]} + > + <Select placeholder="select your language"> + <Option value="English">English</Option> + <Option value="Chinese">Chinese</Option> + </Select> + </Form.Item> <Form.Item name="permission" label="Permissions" @@ -207,7 +218,6 @@ const Configuration = () => { </Form.Item> ); } - return null; }} </Form.Item> diff --git a/web/src/pages/setting/model.ts b/web/src/pages/setting/model.ts index 381380d..79f1e92 100644 --- a/web/src/pages/setting/model.ts +++ b/web/src/pages/setting/model.ts @@ -1,5 +1,9 @@ import { ITenantInfo } from '@/interfaces/database/knowledge'; -import { IThirdOAIModelCollection as IThirdAiModelCollection } from '@/interfaces/database/llm'; +import { + IFactory, + IMyLlmValue, + IThirdOAIModelCollection as IThirdAiModelCollection, +} from '@/interfaces/database/llm'; import { IUserInfo } from '@/interfaces/database/userSetting'; import userService from '@/services/userService'; import { message } from 'antd'; @@ -9,13 +13,12 @@ import { DvaModel } from 'umi'; export interface SettingModelState { isShowPSwModal: boolean; isShowTntModal: boolean; - isShowSAKModal: boolean; isShowSSModal: boolean; llm_factory: string; tenantIfo: Nullable<ITenantInfo>; llmInfo: IThirdAiModelCollection; - myLlm: any[]; - factoriesList: any[]; + myLlmList: Record<string, IMyLlmValue>; + factoryList: IFactory[]; userInfo: IUserInfo; } @@ -24,13 +27,12 @@ const model: DvaModel<SettingModelState> = { state: { isShowPSwModal: false, isShowTntModal: false, - isShowSAKModal: false, isShowSSModal: false, llm_factory: '', tenantIfo: null, llmInfo: {}, - myLlm: [], - factoriesList: [], + myLlmList: {}, + factoryList: [], userInfo: {} as IUserInfo, }, reducers: { @@ -116,16 +118,13 @@ const model: DvaModel<SettingModelState> = { }, *factories_list({ payload = {} }, { call, put }) { - const { data, response } = yield call( - userService.factories_list, - payload, - ); - const { retcode, data: res, retmsg } = data; + const { data } = yield call(userService.factories_list); + const { retcode, data: res } = data; if (retcode === 0) { yield put({ type: 'updateState', payload: { - factoriesList: res, + factoryList: res, }, }); } @@ -143,13 +142,13 @@ const model: DvaModel<SettingModelState> = { } }, *my_llm({ payload = {} }, { call, put }) { - const { data, response } = yield call(userService.my_llm, payload); - const { retcode, data: res, retmsg } = data; + const { data } = yield call(userService.my_llm, payload); + const { retcode, data: res } = data; if (retcode === 0) { yield put({ type: 'updateState', payload: { - myLlm: res, + myLlmList: res, }, }); } @@ -158,14 +157,12 @@ const model: DvaModel<SettingModelState> = { const { data } = yield call(userService.set_api_key, payload); const { retcode } = data; if (retcode === 0) { - message.success('设置API KEYć功ďĽ'); + message.success('Modified!'); yield put({ type: 'updateState', - payload: { - isShowSAKModal: false, - }, }); } + return retcode; }, }, }; diff --git a/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx b/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx new file mode 100644 index 0000000..62b403a --- /dev/null +++ b/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx @@ -0,0 +1,78 @@ +import { IModalManagerChildrenProps } from '@/components/modal-manager'; +import { Form, Input, Modal } from 'antd'; +import { useEffect } from 'react'; + +interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> { + loading: boolean; + initialValue: string; + onOk: (name: string) => void; + showModal?(): void; +} + +type FieldType = { + api_key?: string; +}; + +const ApiKeyModal = ({ + visible, + hideModal, + loading, + initialValue, + onOk, +}: IProps) => { + const [form] = Form.useForm(); + + const handleOk = async () => { + const ret = await form.validateFields(); + + return onOk(ret.api_key); + }; + + const handleCancel = () => { + hideModal(); + }; + + const onFinish = (values: any) => { + console.log('Success:', values); + }; + + const onFinishFailed = (errorInfo: any) => { + console.log('Failed:', errorInfo); + }; + + useEffect(() => { + form.setFieldValue('api_key', initialValue); + }, [initialValue, form]); + + return ( + <Modal + title="Modify" + open={visible} + onOk={handleOk} + onCancel={handleCancel} + okButtonProps={{ loading }} + confirmLoading={loading} + > + <Form + name="basic" + labelCol={{ span: 4 }} + wrapperCol={{ span: 20 }} + style={{ maxWidth: 600 }} + onFinish={onFinish} + onFinishFailed={onFinishFailed} + autoComplete="off" + form={form} + > + <Form.Item<FieldType> + label="Api key" + name="api_key" + rules={[{ required: true, message: 'Please input api key!' }]} + > + <Input /> + </Form.Item> + </Form> + </Modal> + ); +}; + +export default ApiKeyModal; diff --git a/web/src/pages/user-setting/setting-model/hooks.ts b/web/src/pages/user-setting/setting-model/hooks.ts new file mode 100644 index 0000000..725f632 --- /dev/null +++ b/web/src/pages/user-setting/setting-model/hooks.ts @@ -0,0 +1,50 @@ +import { useSetModalState } from '@/hooks/commonHooks'; +import { IApiKeySavingParams, useSaveApiKey } from '@/hooks/llmHooks'; +import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import { useCallback, useState } from 'react'; + +type SavingParamsState = Omit<IApiKeySavingParams, 'api_key'>; + +export const useSubmitApiKey = () => { + const [savingParams, setSavingParams] = useState<SavingParamsState>( + {} as SavingParamsState, + ); + const saveApiKey = useSaveApiKey(); + const { + visible: apiKeyVisible, + hideModal: hideApiKeyModal, + showModal: showApiKeyModal, + } = useSetModalState(); + + const onApiKeySavingOk = useCallback( + async (apiKey: string) => { + const ret = await saveApiKey({ ...savingParams, api_key: apiKey }); + + if (ret.retcode === 0) { + hideApiKeyModal(); + } + }, + [hideApiKeyModal, saveApiKey, savingParams], + ); + + const onShowApiKeyModal = useCallback( + (savingParams: SavingParamsState) => { + setSavingParams(savingParams); + showApiKeyModal(); + }, + [showApiKeyModal, setSavingParams], + ); + + const loading = useOneNamespaceEffectsLoading('settingModel', [ + 'set_api_key', + ]); + + return { + saveApiKeyLoading: loading, + initialApiKey: '', + onApiKeySavingOk, + apiKeyVisible, + hideApiKeyModal, + showApiKeyModal: onShowApiKeyModal, + }; +}; diff --git a/web/src/pages/user-setting/setting-model/index.less b/web/src/pages/user-setting/setting-model/index.less new file mode 100644 index 0000000..d91b0c0 --- /dev/null +++ b/web/src/pages/user-setting/setting-model/index.less @@ -0,0 +1,6 @@ +.modelWrapper { + width: 100%; + .factoryOperationWrapper { + text-align: right; + } +} diff --git a/web/src/pages/user-setting/setting-model/index.tsx b/web/src/pages/user-setting/setting-model/index.tsx index fea0ea4..451d233 100644 --- a/web/src/pages/user-setting/setting-model/index.tsx +++ b/web/src/pages/user-setting/setting-model/index.tsx @@ -1,5 +1,132 @@ +import { + useFetchLlmFactoryListOnMount, + useFetchMyLlmListOnMount, +} from '@/hooks/llmHooks'; +import { SettingOutlined } from '@ant-design/icons'; +import { + Avatar, + Button, + Card, + Col, + Divider, + Flex, + List, + Row, + Space, + Tag, +} from 'antd'; +import SettingTitle from '../components/setting-title'; +import ApiKeyModal from './api-key-modal'; +import { useSubmitApiKey } from './hooks'; + +import styles from './index.less'; + const UserSettingModel = () => { - return <div>UserSettingModel</div>; + const factoryList = useFetchLlmFactoryListOnMount(); + const llmList = useFetchMyLlmListOnMount(); + const { + saveApiKeyLoading, + initialApiKey, + onApiKeySavingOk, + apiKeyVisible, + hideApiKeyModal, + showApiKeyModal, + } = useSubmitApiKey(); + + const handleApiKeyClick = (llmFactory: string) => () => { + showApiKeyModal({ llm_factory: llmFactory }); + }; + + return ( + <> + <section className={styles.modelWrapper}> + <SettingTitle + title="Model Setting" + description="Manage your account settings and preferences here." + ></SettingTitle> + <Divider></Divider> + <List + grid={{ gutter: 16, column: 1 }} + dataSource={llmList} + renderItem={(item) => ( + <List.Item> + <Card> + <Row align={'middle'}> + <Col span={12}> + <Flex gap={'middle'} align="center"> + <Avatar shape="square" size="large" src={item.logo} /> + <Flex vertical gap={'small'}> + <b>{item.name}</b> + <div> + {item.tags.split(',').map((x) => ( + <Tag key={x}>{x}</Tag> + ))} + </div> + </Flex> + </Flex> + </Col> + <Col span={12} className={styles.factoryOperationWrapper}> + <Space size={'middle'}> + <Button onClick={handleApiKeyClick(item.name)}> + API-Key + <SettingOutlined /> + </Button> + <Button> + Show more models + <SettingOutlined /> + </Button> + </Space> + </Col> + </Row> + <List + size="small" + dataSource={item.llm} + renderItem={(item) => <List.Item>{item.name}</List.Item>} + /> + </Card> + </List.Item> + )} + /> + <p>Models to be added</p> + <List + grid={{ + gutter: 16, + xs: 1, + sm: 2, + md: 3, + lg: 4, + xl: 4, + xxl: 8, + }} + dataSource={factoryList} + renderItem={(item) => ( + <List.Item> + <Card> + <Flex vertical gap={'large'}> + <Avatar shape="square" size="large" src={item.logo} /> + <Flex vertical gap={'middle'}> + <b>{item.name}</b> + <Space wrap> + {item.tags.split(',').map((x) => ( + <Tag key={x}>{x}</Tag> + ))} + </Space> + </Flex> + </Flex> + </Card> + </List.Item> + )} + /> + </section> + <ApiKeyModal + visible={apiKeyVisible} + hideModal={hideApiKeyModal} + loading={saveApiKeyLoading} + initialValue={initialApiKey} + onOk={onApiKeySavingOk} + ></ApiKeyModal> + </> + ); }; export default UserSettingModel; -- GitLab