### 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)tags/v0.6.0
| @@ -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' }, | |||
| }, | |||
| @@ -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", | |||
| @@ -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" | |||
| > | |||
| @@ -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 | |||
| @@ -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; | |||
| }; | |||
| @@ -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; | |||
| } | |||
| @@ -506,6 +506,7 @@ export default { | |||
| local: 'Local uploads', | |||
| s3: 'S3 uploads', | |||
| preview: 'Preview', | |||
| fileError: 'File error', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -469,6 +469,7 @@ export default { | |||
| local: '本地上傳', | |||
| s3: 'S3 上傳', | |||
| preview: '預覽', | |||
| fileError: '文件錯誤', | |||
| }, | |||
| footer: { | |||
| profile: '“保留所有權利 @ react”', | |||
| @@ -487,6 +487,7 @@ export default { | |||
| local: '本地上传', | |||
| s3: 'S3 上传', | |||
| preview: '预览', | |||
| fileError: '文件错误', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -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', | |||
| @@ -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'}> | |||
| @@ -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" | |||
| > | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -0,0 +1,4 @@ | |||
| .errorWrapper { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| @@ -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; | |||
| @@ -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 }; | |||
| }; | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -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 ( | |||
| @@ -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" | |||
| @@ -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; | |||
| @@ -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 }; | |||
| }; | |||
| @@ -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> | |||
| )} | |||
| @@ -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); | |||
| @@ -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`, | |||
| @@ -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, | |||