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