feat: delete the added model #503 and display an error message when the requested file fails to parse #684 (#708)

### What problem does this PR solve?

feat: delete the added model #503
feat: display an error message when the requested file fails to parse
#684

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-05-10 10:38:39 +08:00 committed by GitHub
parent bef1bbdf3e
commit d65ba3e4d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 720 additions and 1738 deletions

View File

@ -27,7 +27,7 @@ export default defineConfig({
devtool: 'source-map',
proxy: {
'/v1': {
target: 'http://123.60.95.134:9380/',
target: '',
changeOrigin: true,
// pathRewrite: { '^/v1': '/v1' },
},

1883
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,10 +23,10 @@
"js-base64": "^3.7.5",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mammoth": "^1.7.2",
"rc-tween-one": "^3.0.6",
"react-chat-elements": "^12.0.13",
"react-copy-to-clipboard": "^5.1.0",
"react-file-viewer": "^1.2.1",
"react-i18next": "^14.0.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",

View File

@ -34,7 +34,7 @@ const HighlightPopup = ({
) : null;
const DocumentPreviewer = ({ chunk, documentId, visible }: IProps) => {
const url = useGetDocumentUrl(documentId);
const getDocumentUrl = useGetDocumentUrl(documentId);
const { highlights: state, setWidthAndHeight } = useGetChunkHighlights(chunk);
const ref = useRef<(highlight: IHighlight) => void>(() => {});
const [loaded, setLoaded] = useState(false);
@ -55,7 +55,7 @@ const DocumentPreviewer = ({ chunk, documentId, visible }: IProps) => {
return (
<div className={styles.documentContainer}>
<PdfLoader
url={url}
url={getDocumentUrl()}
beforeLoad={<Skeleton active />}
workerSrc="/pdfjs-dist/pdf.worker.min.js"
>

View File

@ -69,6 +69,8 @@ export const FileMimeTypeMap = {
mp4: 'video/mp4',
};
export const Domain = 'demo.ragflow.io';
//#region file preview
export const Images = [
'jpg',
@ -84,7 +86,7 @@ export const Images = [
];
// Without FileViewer
export const ExceptiveType = ['xlsx', 'xls', 'pdf', ...Images];
export const ExceptiveType = ['xlsx', 'xls', 'pdf', 'docx', ...Images];
export const SupportedPreviewDocumentTypes = ['docx', 'csv', ...ExceptiveType];
export const SupportedPreviewDocumentTypes = [...ExceptiveType];
//#endregion

View File

@ -4,7 +4,10 @@ import {
IMyLlmValue,
IThirdOAIModelCollection,
} from '@/interfaces/database/llm';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import {
IAddLlmRequestBody,
IDeleteLlmRequestBody,
} from '@/interfaces/request/llm';
import { sortLLmFactoryListBySpecifiedOrder } from '@/utils/commonUtil';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'umi';
@ -211,7 +214,7 @@ export const useSaveTenantInfo = () => {
export const useAddLlm = () => {
const dispatch = useDispatch();
const saveTenantInfo = useCallback(
const addLlm = useCallback(
(requestBody: IAddLlmRequestBody) => {
return dispatch<any>({
type: 'settingModel/add_llm',
@ -221,5 +224,21 @@ export const useAddLlm = () => {
[dispatch],
);
return saveTenantInfo;
return addLlm;
};
export const useDeleteLlm = () => {
const dispatch = useDispatch();
const deleteLlm = useCallback(
(requestBody: IDeleteLlmRequestBody) => {
return dispatch<any>({
type: 'settingModel/delete_llm',
payload: requestBody,
});
},
[dispatch],
);
return deleteLlm;
};

View File

@ -4,3 +4,8 @@ export interface IAddLlmRequestBody {
model_type: string;
api_base?: string; // chat|embedding|speech2text|image2text
}
export interface IDeleteLlmRequestBody {
llm_factory: string; // Ollama
llm_name: string;
}

View File

@ -506,6 +506,7 @@ export default {
local: 'Local uploads',
s3: 'S3 uploads',
preview: 'Preview',
fileError: 'File error',
},
footer: {
profile: 'All rights reserved @ React',

View File

@ -469,6 +469,7 @@ export default {
local: '本地上傳',
s3: 'S3 上傳',
preview: '預覽',
fileError: '文件錯誤',
},
footer: {
profile: '“保留所有權利 @ react”',

View File

@ -487,6 +487,7 @@ export default {
local: '本地上传',
s3: 'S3 上传',
preview: '预览',
fileError: '文件错误',
},
footer: {
profile: 'All rights reserved @ React',

View File

@ -55,7 +55,7 @@ const PopoverContent = ({ record }: IProps) => {
{
key: 'process_duation',
label: t('processDuration'),
children: record.process_duation,
children: `${record.process_duation} s`,
},
{
key: 'progress_msg',

View File

@ -1,4 +1,5 @@
import LineChart from '@/components/line-chart';
import { Domain } from '@/constants/common';
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common';
import { IDialog, IStats } from '@/interfaces/database/chat';
@ -80,7 +81,9 @@ const ChatOverviewModal = ({
<Flex gap={8} vertical>
{t('serviceApiEndpoint')}
<Paragraph copyable className={styles.linkText}>
https://demo.ragflow.io/v1/api/
https://
{location.hostname === Domain ? Domain : '<YOUR_MACHINE_IP>'}
/v1/api/
</Paragraph>
</Flex>
<Space size={'middle'}>

View File

@ -1,5 +1,6 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import HightLightMarkdown from '@/components/highlight-markdown';
import { Domain } from '@/constants/common';
import { useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common';
import { Card, Modal, Tabs, TabsProps } from 'antd';
@ -15,7 +16,7 @@ const EmbedModal = ({
const text = `
~~~ html
<iframe
src="https://demo.ragflow.io/chat/share?shared_id=${token}"
src="https://${Domain}/chat/share?shared_id=${token}"
style="width: 100%; height: 100%; min-height: 600px"
frameborder="0"
>

View File

@ -0,0 +1,281 @@
// Copyright (c) 2017 PlanGrid, Inc.
.docxViewerWrapper {
overflow-y: scroll;
height: 100%;
width: 100%;
.box {
width: 100%;
height: 100%;
}
:global(.document-container) {
padding: 30px;
width: 700px;
background: white;
margin: auto;
}
html,
bodyaddress,
blockquote,
body,
dd,
div,
dl,
dt,
fieldset,
form,
frame,
frameset,
h1,
h2,
h3,
h4,
h5,
h6,
noframes,
ol,
p,
ul,
center,
dir,
hr,
menu,
pre {
display: block;
unicode-bidi: embed;
}
li {
display: list-item;
list-style-type: disc;
}
head {
display: none;
}
table {
display: table;
}
img {
width: 100%;
}
tr {
display: table-row;
}
thead {
display: table-header-group;
}
tbody {
display: table-row-group;
}
tfoot {
display: table-footer-group;
}
col {
display: table-column;
}
colgroup {
display: table-column-group;
}
th {
display: table-cell;
}
td {
display: table-cell;
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
padding: 0.2em 0.5em;
}
caption {
display: table-caption;
}
th {
font-weight: bolder;
text-align: center;
}
caption {
text-align: center;
}
body {
margin: 8px;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: 0.75em 0;
}
h3 {
font-size: 1.17em;
margin: 0.83em 0;
}
h4,
p,
blockquote,
ul,
fieldset,
form,
ol,
dl,
dir,
menu {
margin: 1.12em 0;
}
h5 {
font-size: 0.83em;
margin: 1.5em 0;
}
h6 {
font-size: 0.75em;
margin: 1.67em 0;
}
h1,
h2,
h3,
h4,
h5,
h6,
b,
strong {
font-weight: bolder;
}
blockquote {
margin-left: 40px;
margin-right: 40px;
}
i,
cite,
em,
var,
address {
font-style: italic;
}
pre,
tt,
code,
kbd,
samp {
font-family: monospace;
}
pre {
white-space: pre;
}
button,
textarea,
input,
select {
display: inline-block;
}
big {
font-size: 1.17em;
}
small,
sub,
sup {
font-size: 0.83em;
}
sub {
vertical-align: sub;
}
sup {
vertical-align: super;
}
table {
border-spacing: 2px;
}
thead,
tbody,
tfoot {
vertical-align: middle;
}
td,
th,
tr {
vertical-align: inherit;
}
s,
strike,
del {
text-decoration: line-through;
}
hr {
border: 1px inset;
}
ol,
ul,
dir,
menu,
dd {
margin-left: 40px;
}
ol {
list-style-type: decimal;
}
ol ul,
ol ul,
ul ol,
ul ol,
ul ul,
ul ul,
ol ol,
ol ol {
margin-top: 0;
margin-bottom: 0;
}
u,
ins {
text-decoration: underline;
}
br:before {
content: '\A';
white-space: pre-line;
}
center {
text-align: center;
}
:link,
:visited {
text-decoration: underline;
}
:focus {
outline: thin dotted invert;
}
/* Begin bidirectionality settings (do not change) */
BDO[DIR='ltr'] {
direction: ltr;
unicode-bidi: bidi-override;
}
BDO[DIR='rtl'] {
direction: rtl;
unicode-bidi: bidi-override;
}
*[DIR='ltr'] {
direction: ltr;
unicode-bidi: embed;
}
*[DIR='rtl'] {
direction: rtl;
unicode-bidi: embed;
}
@media print {
h1 {
page-break-before: always;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
}
ul,
ol,
dl {
page-break-before: avoid;
}
}
}

View File

@ -0,0 +1,25 @@
import { Spin } from 'antd';
import FileError from '../file-error';
import { useFetchDocx } from '../hooks';
import styles from './index.less';
const Docx = ({ filePath }: { filePath: string }) => {
const { succeed, containerRef } = useFetchDocx(filePath);
return (
<>
{succeed ? (
<section className={styles.docxViewerWrapper}>
<div id="docx" ref={containerRef} className={styles.box}>
<Spin />
</div>
</section>
) : (
<FileError></FileError>
)}
</>
);
};
export default Docx;

View File

@ -1,35 +1,19 @@
import jsPreviewExcel from '@js-preview/excel';
import '@js-preview/excel/lib/index.css';
import { useEffect } from 'react';
import FileError from '../file-error';
import { useFetchExcel } from '../hooks';
const Excel = ({ filePath }: { filePath: string }) => {
const fetchDocument = async () => {
const myExcelPreviewer = jsPreviewExcel.init(
document.getElementById('excel'),
);
const jsonFile = new XMLHttpRequest();
jsonFile.open('GET', filePath, true);
jsonFile.send();
jsonFile.responseType = 'arraybuffer';
jsonFile.onreadystatechange = () => {
if (jsonFile.readyState === 4 && jsonFile.status === 200) {
myExcelPreviewer
.preview(jsonFile.response)
.then((res: any) => {
console.log('succeed');
})
.catch((e) => {
console.log('failed', e);
});
}
};
};
const { status, containerRef } = useFetchExcel(filePath);
useEffect(() => {
fetchDocument();
}, []);
return <div id="excel" style={{ height: '100%' }}></div>;
return (
<div
id="excel"
ref={containerRef}
style={{ height: '100%', width: '100%' }}
>
{status || <FileError></FileError>}
</div>
);
};
export default Excel;

View File

@ -0,0 +1,4 @@
.errorWrapper {
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,15 @@
import { Alert, Flex } from 'antd';
import { useTranslate } from '@/hooks/commonHooks';
import styles from './index.less';
const FileError = () => {
const { t } = useTranslate('fileManager');
return (
<Flex align="center" justify="center" className={styles.errorWrapper}>
<Alert type="error" message={<h1>{t('fileError')}</h1>}></Alert>
</Flex>
);
};
export default FileError;

View File

@ -0,0 +1,78 @@
import jsPreviewExcel from '@js-preview/excel';
import axios from 'axios';
import mammoth from 'mammoth';
import { useCallback, useEffect, useRef, useState } from 'react';
const useFetchDocument = () => {
const fetchDocument = useCallback((api: string) => {
return axios.get(api, { responseType: 'arraybuffer' });
}, []);
return fetchDocument;
};
export const useFetchExcel = (filePath: string) => {
const [status, setStatus] = useState(true);
const fetchDocument = useFetchDocument();
const containerRef = useRef<HTMLDivElement>(null);
const fetchDocumentAsync = useCallback(async () => {
let myExcelPreviewer;
if (containerRef.current) {
myExcelPreviewer = jsPreviewExcel.init(containerRef.current);
}
const jsonFile = await fetchDocument(filePath);
myExcelPreviewer
?.preview(jsonFile.data)
.then(() => {
console.log('succeed');
setStatus(true);
})
.catch((e) => {
console.warn('failed', e);
myExcelPreviewer.destroy();
setStatus(false);
});
}, [filePath, fetchDocument]);
useEffect(() => {
fetchDocumentAsync();
}, [fetchDocumentAsync]);
return { status, containerRef };
};
export const useFetchDocx = (filePath: string) => {
const [succeed, setSucceed] = useState(true);
const fetchDocument = useFetchDocument();
const containerRef = useRef<HTMLDivElement>(null);
const fetchDocumentAsync = useCallback(async () => {
const jsonFile = await fetchDocument(filePath);
mammoth
.convertToHtml(
{ arrayBuffer: jsonFile.data },
{ includeDefaultStyleMap: true },
)
.then((result) => {
setSucceed(true);
const docEl = document.createElement('div');
docEl.className = 'document-container';
docEl.innerHTML = result.value;
const container = containerRef.current;
if (container) {
container.innerHTML = docEl.outerHTML;
}
})
.catch((a) => {
setSucceed(false);
console.warn('alexei: something went wrong', a);
});
}, [filePath, fetchDocument]);
useEffect(() => {
fetchDocumentAsync();
}, [fetchDocumentAsync]);
return { succeed, containerRef };
};

View File

@ -1,8 +1,8 @@
import { ExceptiveType, Images } from '@/constants/common';
import { Images } from '@/constants/common';
import { api_host } from '@/utils/api';
import { Flex, Image } from 'antd';
import FileViewer from 'react-file-viewer';
import { useParams, useSearchParams } from 'umi';
import Docx from './docx';
import Excel from './excel';
import Pdf from './pdf';
@ -10,18 +10,12 @@ import styles from './index.less';
// TODO: The interface returns an incorrect content-type for the SVG.
const isNotExceptiveType = (ext: string) => ExceptiveType.indexOf(ext) === -1;
const DocumentViewer = () => {
const { id: documentId } = useParams();
const api = `${api_host}/file/get/${documentId}`;
const [currentQueryParameters] = useSearchParams();
const ext = currentQueryParameters.get('ext');
const onError = (e: any) => {
console.error(e, 'error in file-viewer');
};
return (
<section className={styles.viewerWrapper}>
{Images.includes(ext!) && (
@ -31,9 +25,8 @@ const DocumentViewer = () => {
)}
{ext === 'pdf' && <Pdf url={api}></Pdf>}
{(ext === 'xlsx' || ext === 'xls') && <Excel filePath={api}></Excel>}
{isNotExceptiveType(ext!) && (
<FileViewer fileType={ext} filePath={api} onError={onError} />
)}
{ext === 'docx' && <Docx filePath={api}></Docx>}
</section>
);
};

View File

@ -1,5 +1,6 @@
import { Skeleton } from 'antd';
import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter';
import FileError from '../file-error';
interface IProps {
url: string;
@ -9,11 +10,15 @@ const DocumentPreviewer = ({ url }: IProps) => {
const resetHash = () => {};
return (
<div style={{ width: '100%' }}>
<div style={{ width: '100%', height: '100%' }}>
<PdfLoader
url={url}
beforeLoad={<Skeleton active />}
workerSrc="/pdfjs-dist/pdf.worker.min.js"
errorMessage={<FileError></FileError>}
onError={(e) => {
console.warn(e);
}}
>
{(pdfDocument) => {
return (

View File

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { Icon, useNavigate } from 'umi';
import RightPanel from './right-panel';
import { Domain } from '@/constants/common';
import styles from './index.less';
const Login = () => {
@ -167,7 +168,7 @@ const Login = () => {
Sign in with Google
</div>
</Button> */}
{location.host === 'demo.ragflow.io' && (
{location.host === Domain && (
<Button
block
size="large"

View File

@ -167,6 +167,17 @@ const model: DvaModel<SettingModelState> = {
}
return retcode;
},
*delete_llm({ payload = {} }, { call, put }) {
const { data } = yield call(userService.delete_llm, payload);
const { retcode } = data;
if (retcode === 0) {
message.success(i18n.t('message.deleted'));
yield put({ type: 'my_llm' });
yield put({ type: 'factories_list' });
}
return retcode;
},
},
};
export default model;

View File

@ -1,8 +1,9 @@
import { useSetModalState } from '@/hooks/commonHooks';
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/commonHooks';
import {
IApiKeySavingParams,
ISystemModelSettingSavingParams,
useAddLlm,
useDeleteLlm,
useFetchLlmList,
useSaveApiKey,
useSaveTenantInfo,
@ -164,3 +165,18 @@ export const useSubmitOllama = () => {
selectedLlmFactory,
};
};
export const useHandleDeleteLlm = (llmFactory: string) => {
const deleteLlm = useDeleteLlm();
const showDeleteConfirm = useShowDeleteConfirm();
const handleDeleteLlm = (name: string) => () => {
showDeleteConfirm({
onOk: async () => {
deleteLlm({ llm_factory: llmFactory, llm_name: name });
},
});
};
return { handleDeleteLlm };
};

View File

@ -6,7 +6,11 @@ import {
useFetchLlmFactoryListOnMount,
useFetchMyLlmListOnMount,
} from '@/hooks/llmHooks';
import { SettingOutlined, UserOutlined } from '@ant-design/icons';
import {
CloseCircleOutlined,
SettingOutlined,
UserOutlined,
} from '@ant-design/icons';
import {
Avatar,
Button,
@ -21,6 +25,7 @@ import {
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd';
import { useCallback } from 'react';
@ -28,6 +33,7 @@ import SettingTitle from '../components/setting-title';
import { isLocalLlmFactory } from '../utils';
import ApiKeyModal from './api-key-modal';
import {
useHandleDeleteLlm,
useSelectModelProvidersLoading,
useSubmitApiKey,
useSubmitOllama,
@ -67,6 +73,7 @@ interface IModelCardProps {
const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
const { visible, switchVisible } = useSetModalState();
const { t } = useTranslate('setting');
const { handleDeleteLlm } = useHandleDeleteLlm(item.name);
const handleApiKeyClick = () => {
clickApiKey(item.name);
@ -113,6 +120,11 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
<List.Item>
<Space>
{item.name} <Tag color="#b8b8b8">{item.type}</Tag>
<Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button type={'text'} onClick={handleDeleteLlm(item.name)}>
<CloseCircleOutlined style={{ color: '#D92D20' }} />
</Button>
</Tooltip>
</Space>
</List.Item>
)}

View File

@ -15,6 +15,7 @@ const {
set_api_key,
set_tenant_info,
add_llm,
delete_llm,
} = api;
const methods = {
@ -66,6 +67,10 @@ const methods = {
url: add_llm,
method: 'post',
},
delete_llm: {
url: delete_llm,
method: 'post',
},
} as const;
const userService = registerServer<keyof typeof methods>(methods, request);

View File

@ -18,6 +18,7 @@ export default {
my_llm: `${api_host}/llm/my_llms`,
set_api_key: `${api_host}/llm/set_api_key`,
add_llm: `${api_host}/llm/add_llm`,
delete_llm: `${api_host}/llm/delete_llm`,
// knowledge base
kb_list: `${api_host}/kb/list`,

1
web/typings.d.ts vendored
View File

@ -10,7 +10,6 @@ import { LoginModelState } from '@/pages/login/model';
import { SettingModelState } from '@/pages/user-setting/model';
declare module 'lodash';
declare module 'react-file-viewer';
function useSelector<TState = RootState, TSelected = unknown>(
selector: (state: TState) => TSelected,