index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. import * as React from 'react';
  2. import { generatePath, useParams, useLocation } from 'react-router-dom';
  3. import { observer } from 'mobx-react';
  4. import config, { getHeaders } from '@/apis/config';
  5. import {
  6. Button,
  7. Table,
  8. TableColumnsType,
  9. Modal,
  10. TablePaginationConfig,
  11. Upload,
  12. UploadProps,
  13. message,
  14. Spin,
  15. Row,
  16. Col,
  17. Card
  18. } from 'antd';
  19. import { EditOutlined, DeleteOutlined, InboxOutlined, PlusOutlined, ArrowLeftOutlined, CloseOutlined, BulbOutlined } from '@ant-design/icons';
  20. import InfoModal from './components/InfoModal';
  21. import InfoModalSetting from './components/InfoModalSetting';
  22. import router from '@/router';
  23. import { Record } from './types';
  24. import dayjs from 'dayjs';
  25. import axios from 'axios';
  26. import LocalStorage from '@/LocalStorage';
  27. import store from './store';
  28. import './style.less';
  29. const { Dragger } = Upload;
  30. const KnowledgeLibInfo : React.FC = () => {
  31. const {
  32. state,
  33. init,
  34. onClickModify,
  35. onClickDelete,
  36. onChangePagination,
  37. onClickDocumentDetail,
  38. infoModalOnClickConfirm,
  39. infoModalOnClickCancel,
  40. infoModalSettingOnClickConfirm,
  41. infoModalSettingOnClickCancel,
  42. onClickSettings,
  43. reset
  44. } = store;
  45. const {
  46. knowledge_id,
  47. listLoading,
  48. page,
  49. list,
  50. infoModalOpen,
  51. infoModalId,
  52. infoModalSettingOpen,
  53. infoModalSettingId,
  54. knowledgeDetail,
  55. } = state;
  56. const location = useLocation();
  57. const [ uploadLoading, setUploadLoading ] = React.useState( false );
  58. const params = useParams();
  59. const [ fileList, setFileList ] = React.useState<any[]>( [] );
  60. const [ uploading, setUploading ] = React.useState( false );
  61. const uploadMessageRef = React.useRef<(() => void) | null>( null );
  62. const [ sListFlag, setSListFlag ] = React.useState<boolean>();
  63. const [ cUpdateFlag, setCUpdateFlag ] = React.useState<boolean>();
  64. const [ detailFlag, setDetailFlag ] = React.useState<boolean>();
  65. const [ deleteFlag, setDeleteFlag ] = React.useState<boolean>();
  66. const [ createFlag, setCreateFlag ] = React.useState<boolean>();
  67. const [userInfoAll, setUserInfoAll] = React.useState<any>({});
  68. // 新手引导(知识库文档)整体可见性(持久化到 localStorage)
  69. const [showDocGuide, setShowDocGuide] = React.useState<boolean>(() => localStorage.getItem('knowledgeDocGuideHidden') !== 'true');
  70. const hideDocGuide = () => { localStorage.setItem('knowledgeDocGuideHidden', 'true'); setShowDocGuide(false); };
  71. const props : UploadProps = {
  72. name: 'files',
  73. multiple: true,
  74. action: '/api/deepseek/api/uploadDocument/' + params.knowledgeId,
  75. headers: getHeaders(),
  76. beforeUpload( file, fileList ) {
  77. setUploadLoading( true );
  78. // const allowedExtensions = ['md', 'txt', 'pdf', 'jpg', 'png', 'jpeg', 'docx', 'xlsx', 'pptx', 'eml', 'csv', 'tar', 'gz', 'bz2', 'zip', 'rar', 'jar'];
  79. const allowedExtensions = [ 'txt', 'pdf', 'jpg', 'png', 'jpeg', 'doc', 'docx', 'ppt', 'pptx' ];
  80. // 检查文件类型
  81. for ( const file of fileList ) {
  82. const fileExt = file.name.split( '.' ).pop()?.toLowerCase();
  83. if ( !fileExt || !allowedExtensions.includes( fileExt ) ) {
  84. message.error( `不支持 ${ fileExt } 格式的文件上传` );
  85. setUploadLoading( false );
  86. return Upload.LIST_IGNORE;
  87. }
  88. }
  89. // 检查文件大小
  90. let totalSize = 0;
  91. for ( const file of fileList ) {
  92. const fileExt = file.name.split( '.' ).pop()?.toLowerCase();
  93. const fileSizeMB = file.size / 1024 / 1024;
  94. if ( fileSizeMB > 30 ) {
  95. message.error( '单个文件不能大于30M' );
  96. setUploadLoading( false );
  97. return Upload.LIST_IGNORE;
  98. }
  99. if ( [ 'jpg', 'png', 'jpeg' ].includes( fileExt! ) && fileSizeMB > 5 ) {
  100. message.error( '单张图片不能大于5M' );
  101. setUploadLoading( false );
  102. return Upload.LIST_IGNORE;
  103. }
  104. totalSize += fileSizeMB;
  105. }
  106. if ( totalSize > 125 ) {
  107. message.error( '文件总大小超过125M' );
  108. setUploadLoading( false );
  109. return Upload.LIST_IGNORE;
  110. }
  111. },
  112. onChange( info ) {
  113. const { status, name } = info.file;
  114. if ( status === 'uploading' ) {
  115. // 文件开始上传,显示持续提示
  116. if ( !uploadMessageRef.current ) {
  117. const closeMessage = message.loading(
  118. `为了确保文件 "${name}" 的解析质量,系统需要一些时间进行准备(通常为2-20分钟)。辛苦您耐心等待,您可以继续使用其他功能,只要不关闭当前页面即可。`,
  119. 0 // 0表示不自动关闭
  120. );
  121. uploadMessageRef.current = closeMessage;
  122. }
  123. setUploadLoading( true );
  124. } else if ( status === 'done' ) {
  125. console.log( status, 'status--done' );
  126. console.info( info.file.response, 'info.file.response.data' );
  127. // 关闭上传提示
  128. if ( uploadMessageRef.current ) {
  129. uploadMessageRef.current();
  130. uploadMessageRef.current = null;
  131. }
  132. if ( info.file.response.code === 200 && info.file.response.data === 1 ) {
  133. message.success( `${ info.file.name } file uploaded successfully.` );
  134. init( params.knowledgeId );
  135. }
  136. setUploadLoading( false );
  137. } else if ( status === 'error' ) {
  138. console.log( status, 'status--error' );
  139. // 关闭上传提示
  140. if ( uploadMessageRef.current ) {
  141. uploadMessageRef.current();
  142. uploadMessageRef.current = null;
  143. }
  144. // 检查是否是504超时错误
  145. const response = info.file.response;
  146. const error = info.file.error;
  147. // 检查HTML格式的504错误或其他504错误格式
  148. const responseStr = String(response || '');
  149. const errorStr = String(error || '');
  150. if (responseStr.includes('504') ||
  151. responseStr.includes('Gateway Time-out') ||
  152. responseStr.includes('<h1>504 Gateway Time-out</h1>') ||
  153. errorStr.includes('timeout') ||
  154. errorStr.includes('504')) {
  155. info.file.response = '上传文件超时,请修改文件后再上传';
  156. info.file.error = '上传文件超时,请修改文件后再上传';
  157. message.error( '上传文件超时,请修改文件后再上传' );
  158. } else {
  159. message.error( `${ info.file.name } 文件上传失败` );
  160. }
  161. setUploadLoading( false );
  162. }
  163. },
  164. onDrop( e ) {
  165. console.log( 'Dropped files', e.dataTransfer.files );
  166. },
  167. };
  168. const handleUpload = async () => {
  169. if ( fileList.length === 0 ) return;
  170. setUploading( true );
  171. const formData = new FormData();
  172. // 添加所有文件
  173. fileList.forEach( file => {
  174. if ( file.originFileObj ) {
  175. formData.append( 'files', file.originFileObj );
  176. }
  177. } );
  178. try {
  179. const res = await axios.post( '/api/deepseek/api/uploadDocument/' + params.knowledgeId, formData, {
  180. headers: { 'Content-Type': 'multipart/form-data' }
  181. } );
  182. message.success( `${ fileList.length }个文件上传成功` );
  183. setFileList( [] );
  184. } catch ( err: any ) {
  185. // 检查是否是504超时错误
  186. if (err?.response?.status === 504 || err?.code === 'ECONNABORTED' || String(err).includes('timeout')) {
  187. message.error( '上传文件超时,请修改文件后再上传' );
  188. } else {
  189. message.error( '上传失败' );
  190. }
  191. } finally {
  192. setUploading( false );
  193. }
  194. };
  195. React.useEffect( () => {
  196. init( params.knowledgeId );
  197. const cList = LocalStorage.getStatusFlag( 'deepseek:slice:list' );
  198. setSListFlag( cList );
  199. const cDetail = LocalStorage.getStatusFlag( 'deepseek:config:update' );
  200. setCUpdateFlag( cDetail );
  201. const detail = LocalStorage.getStatusFlag( 'deepseek:document:detail' );
  202. setDetailFlag( detail );
  203. const deleteF = LocalStorage.getStatusFlag( 'deepseek:document:delete' );
  204. setDeleteFlag( deleteF );
  205. const createF = LocalStorage.getStatusFlag( 'deepseek:document:create' );
  206. setCreateFlag( createF );
  207. setUserInfoAll(LocalStorage.getUserInfo());
  208. return () => reset();
  209. }, [] );
  210. const columns : TableColumnsType<Record> = [
  211. {
  212. title: '序号',
  213. dataIndex: 'index',
  214. width: 80,
  215. render: ( text, record, index ) => {
  216. return index + 1;
  217. }
  218. },
  219. {
  220. title: '文件名',
  221. dataIndex: 'name',
  222. width: 300,
  223. sorter: (a, b) => a.name.localeCompare(b.name),
  224. render: ( text, record ) => {
  225. return (
  226. `${ text }`
  227. )
  228. }
  229. },
  230. {
  231. title: '文件大小',
  232. dataIndex: 'length',
  233. width: 100,
  234. render: ( text ) => {
  235. if ( text ) {
  236. const size = ( text / 1024 / 1024 ).toFixed( 2 );
  237. return `${ size } M`;
  238. } else {
  239. return '--'
  240. }
  241. }
  242. },
  243. {
  244. title: '字符数量',
  245. dataIndex: 'wordNum',
  246. width: 100,
  247. render: ( text ) => {
  248. if ( text ) {
  249. return `${ text }`;
  250. } else {
  251. return '--'
  252. }
  253. }
  254. },
  255. {
  256. title: '分段',
  257. dataIndex: 'sliceTotal',
  258. width: 100,
  259. render: ( text ) => {
  260. if ( text ) {
  261. return `${ text }`;
  262. } else {
  263. return '--';
  264. }
  265. }
  266. },
  267. {
  268. title: '上传时间',
  269. dataIndex: 'createTime',
  270. width: 180,
  271. render: ( text ) => {
  272. if ( text ) {
  273. return dayjs( text ).format( 'YYYY-MM-DD HH:mm:ss' );
  274. } else {
  275. return '--';
  276. }
  277. }
  278. },
  279. {
  280. title: '更新时间',
  281. dataIndex: 'updateTime',
  282. width: 180,
  283. render: ( text ) => {
  284. if ( text ) {
  285. return dayjs( text ).format( 'YYYY-MM-DD HH:mm:ss' );
  286. } else {
  287. return '--';
  288. }
  289. }
  290. },
  291. {
  292. title: '操作',
  293. dataIndex: 'operation',
  294. width: 180,
  295. fixed: 'right',
  296. render: ( text, record:any ) => {
  297. return (
  298. <>
  299. { (createFlag||userInfoAll.id===record.createBy) &&
  300. <a
  301. style={ { marginRight: 16 } }
  302. onClick={ () => {
  303. const path = generatePath( '/deepseek/knowledgeLib/:knowledgeId/:createBy/slice/:documentId/:embeddingId', {
  304. knowledgeId: params.knowledgeId as string,
  305. createBy: params.createBy as string,
  306. documentId: record.documentId,
  307. embeddingId: state.knowledgeDetail.embeddingId,
  308. } );
  309. router.navigate( { pathname: path },{state:{ createBy:record.createBy}} );
  310. } }
  311. >
  312. 切片
  313. </a>
  314. }
  315. {
  316. (cUpdateFlag||userInfoAll.id===record.createBy) &&
  317. <a
  318. style={ { marginRight: 16 } }
  319. onClick={ () => {
  320. onClickSettings( record.documentId );
  321. } }
  322. >
  323. 配置
  324. </a>
  325. }
  326. {
  327. (cUpdateFlag||userInfoAll.id===record.createBy) &&
  328. <a
  329. style={ { marginRight: 16 } }
  330. onClick={ () => {
  331. onClickModify( record.documentId );
  332. } }>
  333. <EditOutlined />
  334. </a>
  335. }
  336. {
  337. (deleteFlag||userInfoAll.id===record.createBy) &&
  338. <a
  339. className='text-error'
  340. onClick={ () => {
  341. Modal.confirm( {
  342. title: '删除',
  343. content: `确定删除知识文件:${ record.name }吗?`,
  344. okType: 'danger',
  345. onOk: async () => {
  346. const userInfo = LocalStorage.getUserInfo();
  347. await onClickDelete( record.documentId );
  348. }
  349. } );
  350. } }
  351. >
  352. <DeleteOutlined />
  353. </a>
  354. }
  355. </>
  356. )
  357. }
  358. }
  359. ];
  360. const paginationConfig : TablePaginationConfig = {
  361. // 显示数据总量
  362. showTotal: ( total : number ) => {
  363. return `共 ${ total } 条`;
  364. },
  365. // 展示分页条数切换
  366. showSizeChanger: true,
  367. // 指定每页显示条数
  368. pageSizeOptions: [ '10', '20', '50', '100' ],
  369. // 快速跳转至某页
  370. showQuickJumper: true,
  371. current: page.page,
  372. pageSize: page.size,
  373. total: page.total,
  374. onChange: async ( page, pageSize ) => {
  375. await onChangePagination( page, pageSize );
  376. },
  377. };
  378. return (
  379. <div className='knowledgeLibInfo'>
  380. <Spin spinning={ uploadLoading || listLoading }>
  381. {showDocGuide && (
  382. <div style={{ padding: '12px 20px 12px 20px', background: '#fff' }}>
  383. <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
  384. <div style={{ fontSize: 13, color: '#333', display: 'flex', alignItems: 'center', gap: 6 }}>
  385. <BulbOutlined style={{ color: '#faad14' }} />
  386. 提示:如何上传并管理知识库文档?
  387. </div>
  388. <CloseOutlined onClick={hideDocGuide} style={{ color: '#999', cursor: 'pointer' }} />
  389. </div>
  390. <Row gutter={12}>
  391. <Col xs={24} sm={24} md={12} lg={8}>
  392. <Card
  393. size="small"
  394. bordered={false}
  395. style={{
  396. background: 'linear-gradient(90deg, #e6f4ff 0%, #f0f7ff 100%)',
  397. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  398. borderRadius: 8,
  399. height: '100%'
  400. }}
  401. bodyStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 80 }}
  402. title={<span style={{ fontWeight: 600 }}>上传知识文档</span>}
  403. >
  404. <div style={{ color: '#666', fontSize: 12 }}>支持多种格式(Word、PDF、图片等),单个/总大小有上限,上传后自动处理(约5-10分钟,如遇到超时,可等待几分钟后刷新页面,将自动成功)。</div>
  405. {/* <div style={{ color: '#666', fontSize: 12 }}>支持pdf,doc,docx文件格式上传,建议单个文件不要超过10M,页数控制在100页以内。尽量单文件上传。</div> */}
  406. <div style={{ textAlign: 'right', color: '#1677ff', fontWeight: 600 }}>step 1</div>
  407. </Card>
  408. </Col>
  409. <Col xs={24} sm={24} md={12} lg={8}>
  410. <Card
  411. size="small"
  412. bordered={false}
  413. style={{
  414. background: 'linear-gradient(90deg, #e6fffb 0%, #f0fffe 100%)',
  415. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  416. borderRadius: 8,
  417. height: '100%'
  418. }}
  419. bodyStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 80 }}
  420. title={<span style={{ fontWeight: 600 }}>查看切片与索引</span>}
  421. >
  422. <div style={{ color: '#666', fontSize: 12 }}>自动解析成功后可查看切片数量与索引状态,必要时可重新生成或调整配置。</div>
  423. <div style={{ textAlign: 'right', color: '#1677ff', fontWeight: 600 }}>step 2</div>
  424. </Card>
  425. </Col>
  426. <Col xs={24} sm={24} md={12} lg={8}>
  427. <Card
  428. size="small"
  429. bordered={false}
  430. style={{
  431. background: 'linear-gradient(90deg, #fff7e6 0%, #fffaf0 100%)',
  432. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  433. borderRadius: 8,
  434. height: '100%'
  435. }}
  436. bodyStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 80 }}
  437. title={<span style={{ fontWeight: 600 }}>在应用中生效</span>}
  438. >
  439. <div style={{ color: '#666', fontSize: 12 }}>在RAG应用中切片将会被模型检索并召回,形成大模型对话的依据语料。</div>
  440. <div style={{ textAlign: 'right', color: '#1677ff', fontWeight: 600 }}>step 3</div>
  441. </Card>
  442. </Col>
  443. </Row>
  444. </div>
  445. )}
  446. {
  447. page.total === 0 &&
  448. <div className='knowledgeLibInfo-operation'>
  449. <div>
  450. <Button
  451. type='primary'
  452. icon={ <ArrowLeftOutlined /> }
  453. onClick={ () => {
  454. router.navigate( { pathname: '/deepseek/knowledgeLib' } );
  455. } }
  456. >
  457. 返回
  458. </Button>
  459. </div>
  460. {
  461. ((createFlag&&userInfoAll.id==='1')||userInfoAll.id===params?.createBy) &&
  462. <div style={ { marginTop: 20, width: '100%', height: '200px' } }>
  463. <Dragger { ...props }>
  464. <p className="ant-upload-drag-icon">
  465. <InboxOutlined />
  466. </p>
  467. <p>
  468. 点击上传,或拖放文件到此处
  469. </p>
  470. <p className="ant-upload-hint">
  471. {/* 支持文件格式md,txt,pdf,jpg,png,jpeg,docx,xlsx,
  472. pptx,eml,csv,单个文档 ≤ 30M,单张图片 ≤ 2.5M,文件总
  473. 大小不得超过125M. */}
  474. 支持pdf,doc,docx,jpg,png,jpeg文件格式上传,建议单个文件不要超过10M,页数控制在100页以内。尽量单文件上传。
  475. </p>
  476. </Dragger>
  477. </div>
  478. }
  479. </div>
  480. }
  481. {
  482. page.total > 0 &&
  483. <>
  484. <div className='knowledgeLibInfo-operation'>
  485. <Button
  486. style={ { marginRight: 16 } }
  487. type='primary'
  488. icon={ <ArrowLeftOutlined /> } onClick={ () => {
  489. router.navigate( { pathname: '/deepseek/knowledgeLib' } );
  490. } }>
  491. 返回
  492. </Button>
  493. {
  494. ((createFlag&&userInfoAll.id==='1')||userInfoAll.id===params?.createBy) &&
  495. <Upload { ...props }>
  496. <Button type='primary' icon={ <PlusOutlined /> }>上传知识文件</Button>
  497. </Upload>
  498. }
  499. </div>
  500. <div className='knowledgeLibInfo-table'>
  501. <Table
  502. scroll={ { x: 'max-content' } }
  503. rowKey={ ( record ) => record.documentId }
  504. loading={ listLoading }
  505. columns={ columns }
  506. dataSource={ list }
  507. pagination={ paginationConfig }
  508. />
  509. </div>
  510. {
  511. infoModalOpen &&
  512. <InfoModal
  513. id={ infoModalId }
  514. open={ infoModalOpen }
  515. onClickConfirm={ infoModalOnClickConfirm }
  516. onClickCancel={ infoModalOnClickCancel }
  517. />
  518. }
  519. {
  520. infoModalSettingOpen &&
  521. <InfoModalSetting
  522. id={ infoModalSettingId }
  523. open={ infoModalSettingOpen }
  524. onClickConfirm={ infoModalSettingOnClickConfirm }
  525. onClickCancel={ infoModalSettingOnClickCancel }
  526. />
  527. }
  528. </>
  529. }
  530. </Spin>
  531. </div>
  532. );
  533. };
  534. export default observer( KnowledgeLibInfo );