index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import * as React from 'react';
  2. import { observer } from 'mobx-react';
  3. import { generatePath, useLocation, useParams } from 'react-router-dom';
  4. import { Form, Input, Button, message, Upload, UploadProps } from 'antd';
  5. import { apis } from '@/apis';
  6. import router from '@/router';
  7. import { ArrowLeftOutlined } from '@ant-design/icons';
  8. import config, { getHeaders } from '@/apis/config';
  9. const { TextArea } = Input;
  10. import { useState, useRef, useEffect } from 'react';
  11. import { EyeOutlined, CloseOutlined } from '@ant-design/icons';
  12. const FormItem = Form.Item;
  13. // 引入富文本
  14. import MarkdownIt from "markdown-it";
  15. import MdEditor from "react-markdown-editor-lite";
  16. import "react-markdown-editor-lite/lib/index.css";
  17. import './style.less';
  18. // 初始化 markdown-it
  19. const mdParser = new MarkdownIt({
  20. html: true,
  21. typographer: true
  22. });
  23. const ar2 = [
  24. {
  25. name:'我是图片1',
  26. url:'https://jkeckms.ryuiso.com/chat-bg.jpg'
  27. },
  28. {
  29. name:'我是图片2',
  30. url:'https://jkeckms.ryuiso.com/chat-bg.jpg'
  31. },
  32. {
  33. name:'我是图片3',
  34. url:'https://jkeckms.ryuiso.com/chat-bg.jpg'
  35. }
  36. ]
  37. interface MdImg {
  38. name: string;
  39. url: string;
  40. }
  41. const SliceDetail: React.FC = () => {
  42. const [form] = Form.useForm();
  43. const params = useParams();
  44. const location = useLocation();
  45. // console.log('location------',location);
  46. const { text = '', page } = location.state??{};
  47. const [listLoading, setListLoading] = React.useState(false);
  48. const [mdImgUrlList, setMdImgUrlList] = useState<MdImg[]>();
  49. const appApi = {
  50. fetchList: async () => {
  51. setListLoading(true);
  52. try {
  53. if (!params.sliceId || !params.knowledgeId) {
  54. throw new Error('参数错误');
  55. }
  56. const res = await apis.fetchTakaiSliceDetail(params.sliceId, params.knowledgeId);
  57. const info = res.data.data;
  58. form.setFieldsValue({
  59. slice_id: info.slice_id,
  60. slice_text: info.slice_text,
  61. document_id: info.document_id,
  62. })
  63. setContentMd(info.slice_text || '');
  64. setMdImgUrlList(info.imageList || []);
  65. } catch (error) {
  66. console.error(error);
  67. } finally {
  68. setListLoading(false);
  69. }
  70. }
  71. };
  72. const init = async () => {
  73. if (params.sliceId && params.sliceId !== 'new') {
  74. await appApi.fetchList();
  75. }
  76. }
  77. React.useEffect(() => {
  78. init();
  79. }, [])
  80. const [cursorEndPosition, setCursorEndPosition] = React.useState<number>(0);
  81. // 上传图片配置
  82. const uploadImageConfig: UploadProps = {
  83. name: 'files',
  84. action: config.baseURL + `/deepseek/api/uploadSliceImage/${params.knowledgeId}/${params.documentId}`,
  85. method: 'POST',
  86. headers: getHeaders(),
  87. accept: ['.png', '.jpg', '.jpeg'].join(','),
  88. multiple: true,
  89. maxCount: 5,
  90. showUploadList: false,
  91. };
  92. // 处理markdown编辑器
  93. const [contentMd, setContentMd] = React.useState('');
  94. // 在这里做关键字替换
  95. const customRender = (text: string)=> {
  96. let html = mdParser.render(text);
  97. // 比如:把 "我是图片" 替换成一张图片
  98. mdImgUrlList?.forEach(item=>{
  99. html = html.replace(new RegExp(item.name, 'g'), `<img src="${item.url}" alt="${item.name}" />`);
  100. })
  101. // html = html.replace(/我是图片/g, `<img src="https://jkeckms.ryuiso.com/chat-bg.jpg" alt="我是图片" />`);
  102. // 你还可以做更多规则,比如匹配 [img:xxx] 这种自定义语法
  103. // html = html.replace(/\[img:(.+?)\]/g, (_, name) => `<img src="/images/${name}.png" />`);
  104. form.setFieldsValue({ slice_text: text });
  105. return html;
  106. }
  107. return (
  108. <div>
  109. <div className='questionAnswerList'>
  110. <div style={{ height: '100%', marginLeft: '10px' }}>
  111. <Form
  112. style={{ paddingTop: '20px', height: '100%' }}
  113. form={form}
  114. layout='vertical'
  115. >
  116. <Form.Item
  117. // name="slice_text"
  118. rules={[{ required: true, message: '内容不能为空' }]}
  119. >
  120. <MdEditor
  121. config={{
  122. view:{
  123. menu:false
  124. }
  125. }}
  126. value={contentMd}
  127. style={{ height: "450px",width:'98%' }}
  128. renderHTML={customRender}
  129. onChange={({ text }) => setContentMd(text)}
  130. onBlur={(e: React.FocusEvent<HTMLTextAreaElement>) => {
  131. const ta = e.target as HTMLTextAreaElement;
  132. const start = ta.selectionStart;
  133. const end = ta.selectionEnd;
  134. setCursorEndPosition(end);
  135. // setCursor({ start, end });
  136. // console.log('blur cursor', start, end, ta.value);
  137. }}
  138. />
  139. </Form.Item>
  140. {false && <FormItem
  141. name="slice_text"
  142. rules={[{ required: true, message: '切片内容不能为空' }]}>
  143. <TextArea
  144. style={{
  145. width: '50%',
  146. height: '200px',
  147. overflow: 'auto',
  148. resize: 'both',
  149. boxSizing: 'border-box',
  150. }}
  151. placeholder=""
  152. autoSize={{ minRows: 20, maxRows: 5000 }}
  153. onBlur={(e) => {
  154. const target = e.target as HTMLTextAreaElement;
  155. // 更新光标终点位置
  156. setCursorEndPosition(target.selectionEnd);
  157. }}
  158. />
  159. </FormItem>}
  160. <Upload
  161. {...uploadImageConfig}
  162. onChange={(info) => {
  163. const insertToSliceText = (text: string) => {
  164. const slice_text = contentMd
  165. // 获取当前光标位置
  166. const position = cursorEndPosition;
  167. let newValue = '';
  168. if (!slice_text) {
  169. newValue = text;
  170. } else {
  171. newValue = slice_text.slice(0, position) + text + slice_text.slice(position);
  172. }
  173. setContentMd(newValue);
  174. form.setFieldsValue({ slice_text: newValue });
  175. }
  176. const file = info.file;
  177. if (file.status === 'done') {// 上传成功
  178. const { code, msg, data } = file.response;
  179. if (code === 200) {
  180. const text = data&&data[0]?.name||'';
  181. insertToSliceText(text);
  182. const flagList = mdImgUrlList || [];
  183. const flagList1 = [...flagList,...data];
  184. setMdImgUrlList(flagList1)
  185. message.success('上传成功');
  186. } else {
  187. message.error(msg);
  188. }
  189. } else if (file.status === 'error') {// 上传失败
  190. message.error('上传失败');
  191. }
  192. }}
  193. >
  194. <Button type='primary'>
  195. 解析图片
  196. </Button>
  197. </Upload>
  198. <Button
  199. style={{ margin: '0 16px' }}
  200. type='primary'
  201. icon={<ArrowLeftOutlined />}
  202. onClick={() => {
  203. const path = generatePath('/deepseek/knowledgeLib/:knowledgeId/:createBy/slice/:documentId/:embeddingId', {
  204. knowledgeId: params.knowledgeId as string,
  205. documentId: params.documentId as string,
  206. createBy: params.createBy as string,
  207. embeddingId: params.embeddingId as string,
  208. });
  209. router.navigate({ pathname: path },
  210. { state: { text: text, page } }
  211. );
  212. }}
  213. >
  214. 返回
  215. </Button>
  216. <Button
  217. type='primary'
  218. onClick={() => {
  219. form.validateFields().then(async (values) => {
  220. values = {
  221. slice_text:contentMd
  222. }
  223. // 验证参数是否存在
  224. if (!params.knowledgeId || !params.sliceId) {
  225. message.error('知识库ID或切片ID无效');
  226. return;
  227. }
  228. const data = values;
  229. const info = {
  230. knowledgeId: params.knowledgeId,
  231. sliceId: params.sliceId === null ? '' : params.sliceId,
  232. sliceText: values.slice_text,
  233. documentId: params.documentId,
  234. };
  235. let res = null;
  236. if (params.sliceId && params.sliceId !== 'null' && params.sliceId !== 'new') {
  237. // 编辑应用
  238. res = await apis.modifyTakaiSliceInfo(info as any);
  239. } else {
  240. res = await apis.addTakaiSlice(info as any);
  241. }
  242. if (res.code === 200 && res.data === 1) {
  243. if (params.sliceId && params.sliceId !== 'null' && params.sliceId !== 'new') {
  244. message.success('修改成功');
  245. } else {
  246. message.success('新增成功');
  247. }
  248. } else {
  249. if (params.sliceId && params.sliceId !== 'null' && params.sliceId !== 'new') {
  250. message.error('修改失败');
  251. } else {
  252. message.error('新增失败');
  253. }
  254. }
  255. const path = generatePath('/deepseek/knowledgeLib/:knowledgeId/:createBy/slice/:documentId/:embeddingId', {
  256. knowledgeId: params.knowledgeId as string,
  257. createBy: params.createBy as string,
  258. documentId: params.documentId as string,
  259. embeddingId: params.embeddingId as string,
  260. });
  261. router.navigate({ pathname: path }, {
  262. state: { text: text, page }
  263. });
  264. }).catch((error) => {
  265. console.error(error);
  266. });
  267. }}
  268. >
  269. 保存
  270. </Button>
  271. </Form>
  272. </div>
  273. </div>
  274. </div>
  275. )
  276. };
  277. export default observer(SliceDetail);