index.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { apiInterceptors, chunkAddQuestion, getChunkList } from '@/client/api';
  2. import MenuModal from '@/components/MenuModal';
  3. import MarkDownContext from '@/new-components/common/MarkdownContext';
  4. import { MinusCircleOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
  5. import { useRequest } from 'ahooks';
  6. import { App, Breadcrumb, Button, Card, Empty, Form, Input, Pagination, Space, Spin, Tag } from 'antd';
  7. import cls from 'classnames';
  8. import { debounce } from 'lodash';
  9. import { useRouter } from 'next/router';
  10. import { useEffect, useState } from 'react';
  11. import { useTranslation } from 'react-i18next';
  12. const DEDAULT_PAGE_SIZE = 10;
  13. function ChunkList() {
  14. const router = useRouter();
  15. const { t } = useTranslation();
  16. const [chunkList, setChunkList] = useState<any>([]);
  17. const [total, setTotal] = useState<number>(0);
  18. const [loading, setLoading] = useState<boolean>(false);
  19. // const [isExpand, setIsExpand] = useState<boolean>(false);
  20. const [isModalOpen, setIsModalOpen] = useState(false);
  21. const [currentChunkInfo, setCurrentChunkInfo] = useState<any>(null);
  22. const [currentPage, setCurrentPage] = useState(1);
  23. const [pageSize, setPageSize] = useState<number>(10);
  24. const [form] = Form.useForm();
  25. const { message } = App.useApp();
  26. const {
  27. query: { id, spaceName },
  28. } = useRouter();
  29. const fetchChunks = async () => {
  30. setLoading(true);
  31. const [_, data] = await apiInterceptors(
  32. getChunkList(spaceName as string, {
  33. document_id: id as string,
  34. page: 1,
  35. page_size: DEDAULT_PAGE_SIZE,
  36. }),
  37. );
  38. setChunkList(data?.data);
  39. setTotal(data?.total ?? 0);
  40. setLoading(false);
  41. };
  42. const loaderMore = async (page: number, page_size: number) => {
  43. setPageSize(page_size);
  44. setLoading(true);
  45. const [_, data] = await apiInterceptors(
  46. getChunkList(spaceName as string, {
  47. document_id: id as string,
  48. page,
  49. page_size,
  50. }),
  51. );
  52. setChunkList(data?.data || []);
  53. setLoading(false);
  54. setCurrentPage(page);
  55. };
  56. useEffect(() => {
  57. spaceName && id && fetchChunks();
  58. // eslint-disable-next-line react-hooks/exhaustive-deps
  59. }, [id, spaceName]);
  60. const onSearch = async (e: any) => {
  61. const content = e.target.value;
  62. if (!content) {
  63. return;
  64. }
  65. const [_, data] = await apiInterceptors(
  66. getChunkList(spaceName as string, {
  67. document_id: id as string,
  68. page: currentPage,
  69. page_size: pageSize,
  70. content,
  71. }),
  72. );
  73. setChunkList(data?.data || []);
  74. };
  75. // 添加问题
  76. const { run: addQuestionRun, loading: addLoading } = useRequest(
  77. async (questions: string[]) => apiInterceptors(chunkAddQuestion({ chunk_id: currentChunkInfo.id, questions })),
  78. {
  79. manual: true,
  80. onSuccess: async () => {
  81. message.success('添加成功');
  82. setIsModalOpen(false);
  83. await fetchChunks();
  84. },
  85. },
  86. );
  87. return (
  88. <div className='flex flex-col h-full w-full px-6 pb-6'>
  89. <Breadcrumb
  90. className='m-6'
  91. items={[
  92. {
  93. title: 'Knowledge',
  94. onClick() {
  95. router.back();
  96. },
  97. path: '/knowledge',
  98. },
  99. {
  100. title: spaceName,
  101. },
  102. ]}
  103. />
  104. <div className='flex items-center gap-4'>
  105. <Input
  106. className='w-1/5 h-10 mb-4'
  107. prefix={<SearchOutlined />}
  108. placeholder={t('please_enter_the_keywords')}
  109. onChange={debounce(onSearch, 300)}
  110. allowClear
  111. />
  112. </div>
  113. {chunkList?.length > 0 ? (
  114. <div className='h-full grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 grid-flow-row auto-rows-max gap-x-6 gap-y-10 overflow-y-auto relative'>
  115. <Spin
  116. className='flex flex-col items-center justify-center absolute bottom-0 top-0 left-0 right-0'
  117. spinning={loading}
  118. />
  119. {chunkList?.map((chunk: any, index: number) => {
  120. return (
  121. <Card
  122. hoverable
  123. key={chunk.id}
  124. title={
  125. <Space className='flex justify-between'>
  126. <Tag color='blue'># {index + (currentPage - 1) * DEDAULT_PAGE_SIZE}</Tag>
  127. {/* <DocIcon type={chunk.doc_type} /> */}
  128. <span className='text-sm'>{chunk.doc_name}</span>
  129. </Space>
  130. }
  131. className={cls('h-96 rounded-xl overflow-hidden', {
  132. // 'h-auto': isExpand,
  133. 'h-auto': true,
  134. })}
  135. onClick={() => {
  136. setIsModalOpen(true);
  137. setCurrentChunkInfo(chunk);
  138. }}
  139. >
  140. <p className='font-semibold'>{t('Content')}:</p>
  141. <p>{chunk?.content}</p>
  142. <p className='font-semibold'>{t('Meta_Data')}: </p>
  143. <p>{chunk?.meta_info}</p>
  144. {/* <Space
  145. className="absolute bottom-0 right-0 left-0 flex items-center justify-center cursor-pointer text-[#1890ff] bg-[rgba(255,255,255,0.8)] z-30"
  146. onClick={() => setIsExpand(!isExpand)}
  147. >
  148. <DoubleRightOutlined rotate={isExpand ? -90 : 90} /> {isExpand ? '收起' : '展开'}
  149. </Space> */}
  150. </Card>
  151. );
  152. })}
  153. </div>
  154. ) : (
  155. <Spin spinning={loading}>
  156. <Empty image={Empty.PRESENTED_IMAGE_DEFAULT} />
  157. </Spin>
  158. )}
  159. <Pagination
  160. className='flex w-full justify-end'
  161. defaultCurrent={1}
  162. defaultPageSize={DEDAULT_PAGE_SIZE}
  163. total={total}
  164. showTotal={total => `Total ${total} items`}
  165. onChange={loaderMore}
  166. />
  167. <MenuModal
  168. modal={{
  169. title: t('Manual_entry'),
  170. width: '70%',
  171. open: isModalOpen,
  172. footer: false,
  173. onCancel: () => setIsModalOpen(false),
  174. afterOpenChange: open => {
  175. if (open) {
  176. form.setFieldValue(
  177. 'questions',
  178. JSON.parse(currentChunkInfo?.questions || '[]')?.map((item: any) => ({ question: item })),
  179. );
  180. }
  181. },
  182. }}
  183. items={[
  184. {
  185. key: 'edit',
  186. label: t('Data_content'),
  187. children: (
  188. <div className='flex gap-4'>
  189. <Card size='small' title={t('Main_content')} className='w-2/3 flex-wrap overflow-y-auto'>
  190. <MarkDownContext>{currentChunkInfo?.content}</MarkDownContext>
  191. </Card>
  192. <Card size='small' title={t('Auxiliary_data')} className='w-1/3'>
  193. <MarkDownContext>{currentChunkInfo?.meta_info}</MarkDownContext>
  194. </Card>
  195. </div>
  196. ),
  197. },
  198. {
  199. key: 'delete',
  200. label: t('Add_problem'),
  201. children: (
  202. <Card
  203. size='small'
  204. extra={
  205. <Button
  206. size='small'
  207. type='primary'
  208. onClick={async () => {
  209. const formVal = form.getFieldsValue();
  210. if (!formVal.questions) {
  211. message.warning(t('enter_question_first'));
  212. return;
  213. }
  214. if (formVal.questions?.filter(Boolean).length === 0) {
  215. message.warning(t('enter_question_first'));
  216. return;
  217. }
  218. const questions = formVal.questions?.filter(Boolean).map((item: any) => item.question);
  219. await addQuestionRun(questions);
  220. }}
  221. loading={addLoading}
  222. >
  223. {t('save')}
  224. </Button>
  225. }
  226. >
  227. <Form form={form}>
  228. <Form.List name='questions'>
  229. {(fields, { add, remove }) => (
  230. <>
  231. {fields.map(({ key, name }) => (
  232. <div key={key} className={cls('flex flex-1 items-center gap-8')}>
  233. <Form.Item label='' name={[name, 'question']} className='grow'>
  234. <Input placeholder={t('Please_Input')} />
  235. </Form.Item>
  236. <Form.Item>
  237. <MinusCircleOutlined
  238. onClick={() => {
  239. remove(name);
  240. }}
  241. />
  242. </Form.Item>
  243. </div>
  244. ))}
  245. <Form.Item>
  246. <Button
  247. type='dashed'
  248. onClick={() => {
  249. add();
  250. }}
  251. block
  252. icon={<PlusOutlined />}
  253. >
  254. {t('Add_problem')}
  255. </Button>
  256. </Form.Item>
  257. </>
  258. )}
  259. </Form.List>
  260. </Form>
  261. </Card>
  262. ),
  263. },
  264. ]}
  265. />
  266. </div>
  267. );
  268. }
  269. export default ChunkList;