您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import { useTranslate } from '@/hooks/common-hooks';
  2. import {
  3. useDeleteDocument,
  4. useFetchDocumentInfosByIds,
  5. useRemoveNextDocument,
  6. useUploadAndParseDocument,
  7. } from '@/hooks/document-hooks';
  8. import { getExtension } from '@/utils/document-util';
  9. import { formatBytes } from '@/utils/file-util';
  10. import {
  11. CloseCircleOutlined,
  12. InfoCircleOutlined,
  13. LoadingOutlined,
  14. } from '@ant-design/icons';
  15. import type { GetProp, UploadFile } from 'antd';
  16. import {
  17. Button,
  18. Card,
  19. Flex,
  20. Input,
  21. List,
  22. Space,
  23. Spin,
  24. Typography,
  25. Upload,
  26. UploadProps,
  27. } from 'antd';
  28. import classNames from 'classnames';
  29. import get from 'lodash/get';
  30. import {
  31. ChangeEventHandler,
  32. memo,
  33. useCallback,
  34. useEffect,
  35. useRef,
  36. useState,
  37. } from 'react';
  38. import FileIcon from '../file-icon';
  39. import SvgIcon from '../svg-icon';
  40. import styles from './index.less';
  41. type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
  42. const { Text } = Typography;
  43. const getFileId = (file: UploadFile) => get(file, 'response.data.0');
  44. const getFileIds = (fileList: UploadFile[]) => {
  45. const ids = fileList.reduce((pre, cur) => {
  46. return pre.concat(get(cur, 'response.data', []));
  47. }, []);
  48. return ids;
  49. };
  50. const isUploadError = (file: UploadFile) => {
  51. const retcode = get(file, 'response.retcode');
  52. return typeof retcode === 'number' && retcode !== 0;
  53. };
  54. const isUploadSuccess = (file: UploadFile) => {
  55. const retcode = get(file, 'response.retcode');
  56. return typeof retcode === 'number' && retcode === 0;
  57. };
  58. interface IProps {
  59. disabled: boolean;
  60. value: string;
  61. sendDisabled: boolean;
  62. sendLoading: boolean;
  63. onPressEnter(documentIds: string[]): void;
  64. onInputChange: ChangeEventHandler<HTMLInputElement>;
  65. conversationId: string;
  66. uploadMethod?: string;
  67. isShared?: boolean;
  68. showUploadIcon?: boolean;
  69. createConversationBeforeUploadDocument?(message: string): Promise<any>;
  70. }
  71. const getBase64 = (file: FileType): Promise<string> =>
  72. new Promise((resolve, reject) => {
  73. const reader = new FileReader();
  74. reader.readAsDataURL(file as any);
  75. reader.onload = () => resolve(reader.result as string);
  76. reader.onerror = (error) => reject(error);
  77. });
  78. const MessageInput = ({
  79. isShared = false,
  80. disabled,
  81. value,
  82. onPressEnter,
  83. sendDisabled,
  84. sendLoading,
  85. onInputChange,
  86. conversationId,
  87. showUploadIcon = true,
  88. createConversationBeforeUploadDocument,
  89. uploadMethod = 'upload_and_parse',
  90. }: IProps) => {
  91. const { t } = useTranslate('chat');
  92. const { removeDocument } = useRemoveNextDocument();
  93. const { deleteDocument } = useDeleteDocument();
  94. const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
  95. const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod);
  96. const conversationIdRef = useRef(conversationId);
  97. const [fileList, setFileList] = useState<UploadFile[]>([]);
  98. const handlePreview = async (file: UploadFile) => {
  99. if (!file.url && !file.preview) {
  100. file.preview = await getBase64(file.originFileObj as FileType);
  101. }
  102. };
  103. const handleChange: UploadProps['onChange'] = async ({
  104. // fileList: newFileList,
  105. file,
  106. }) => {
  107. let nextConversationId: string = conversationId;
  108. if (createConversationBeforeUploadDocument) {
  109. const creatingRet = await createConversationBeforeUploadDocument(
  110. file.name,
  111. );
  112. if (creatingRet.retcode === 0) {
  113. nextConversationId = creatingRet.data.id;
  114. }
  115. }
  116. setFileList((list) => {
  117. list.push({
  118. ...file,
  119. status: 'uploading',
  120. originFileObj: file as any,
  121. });
  122. return [...list];
  123. });
  124. const ret = await uploadAndParseDocument({
  125. conversationId: nextConversationId,
  126. fileList: [file],
  127. });
  128. setFileList((list) => {
  129. const nextList = list.filter((x) => x.uid !== file.uid);
  130. nextList.push({
  131. ...file,
  132. originFileObj: file as any,
  133. response: ret,
  134. percent: 100,
  135. status: ret?.retcode === 0 ? 'done' : 'error',
  136. });
  137. return nextList;
  138. });
  139. };
  140. const isUploadingFile = fileList.some((x) => x.status === 'uploading');
  141. const handlePressEnter = useCallback(async () => {
  142. if (isUploadingFile) return;
  143. const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
  144. onPressEnter(ids);
  145. setFileList([]);
  146. }, [fileList, onPressEnter, isUploadingFile]);
  147. const handleRemove = useCallback(
  148. async (file: UploadFile) => {
  149. const ids = get(file, 'response.data', []);
  150. // Upload Successfully
  151. if (Array.isArray(ids) && ids.length) {
  152. if (isShared) {
  153. await deleteDocument(ids);
  154. } else {
  155. await removeDocument(ids[0]);
  156. }
  157. setFileList((preList) => {
  158. return preList.filter((x) => getFileId(x) !== ids[0]);
  159. });
  160. } else {
  161. // Upload failed
  162. setFileList((preList) => {
  163. return preList.filter((x) => x.uid !== file.uid);
  164. });
  165. }
  166. },
  167. [removeDocument, deleteDocument, isShared],
  168. );
  169. const getDocumentInfoById = useCallback(
  170. (id: string) => {
  171. return documentInfos.find((x) => x.id === id);
  172. },
  173. [documentInfos],
  174. );
  175. useEffect(() => {
  176. const ids = getFileIds(fileList);
  177. setDocumentIds(ids);
  178. }, [fileList, setDocumentIds]);
  179. useEffect(() => {
  180. if (
  181. conversationIdRef.current &&
  182. conversationId !== conversationIdRef.current
  183. ) {
  184. setFileList([]);
  185. }
  186. conversationIdRef.current = conversationId;
  187. }, [conversationId, setFileList]);
  188. return (
  189. <Flex gap={20} vertical className={styles.messageInputWrapper}>
  190. <Input
  191. size="large"
  192. placeholder={t('sendPlaceholder')}
  193. value={value}
  194. disabled={disabled}
  195. className={classNames({ [styles.inputWrapper]: fileList.length === 0 })}
  196. suffix={
  197. <Space>
  198. {showUploadIcon && (
  199. <Upload
  200. // action={uploadUrl}
  201. // fileList={fileList}
  202. onPreview={handlePreview}
  203. onChange={handleChange}
  204. multiple={false}
  205. // headers={{ [Authorization]: getAuthorization() }}
  206. // data={{ conversation_id: conversationId }}
  207. // method="post"
  208. onRemove={handleRemove}
  209. showUploadList={false}
  210. beforeUpload={(file, fileList) => {
  211. console.log('🚀 ~ beforeUpload:', fileList);
  212. return false;
  213. }}
  214. >
  215. <Button
  216. type={'text'}
  217. disabled={disabled}
  218. icon={
  219. <SvgIcon
  220. name="paper-clip"
  221. width={18}
  222. height={22}
  223. disabled={disabled}
  224. ></SvgIcon>
  225. }
  226. ></Button>
  227. </Upload>
  228. )}
  229. <Button
  230. type="primary"
  231. onClick={handlePressEnter}
  232. loading={sendLoading}
  233. disabled={sendDisabled || isUploadingFile}
  234. >
  235. {t('send')}
  236. </Button>
  237. </Space>
  238. }
  239. onPressEnter={handlePressEnter}
  240. onChange={onInputChange}
  241. />
  242. {fileList.length > 0 && (
  243. <List
  244. grid={{
  245. gutter: 16,
  246. xs: 1,
  247. sm: 1,
  248. md: 1,
  249. lg: 1,
  250. xl: 2,
  251. xxl: 4,
  252. }}
  253. dataSource={fileList}
  254. className={styles.listWrapper}
  255. renderItem={(item) => {
  256. const id = getFileId(item);
  257. const documentInfo = getDocumentInfoById(id);
  258. const fileExtension = getExtension(documentInfo?.name ?? '');
  259. const fileName = item.originFileObj?.name ?? '';
  260. return (
  261. <List.Item>
  262. <Card className={styles.documentCard}>
  263. <Flex gap={10} align="center">
  264. {item.status === 'uploading' || !item.response ? (
  265. <Spin
  266. indicator={
  267. <LoadingOutlined style={{ fontSize: 24 }} spin />
  268. }
  269. />
  270. ) : !getFileId(item) ? (
  271. <InfoCircleOutlined
  272. size={30}
  273. // width={30}
  274. ></InfoCircleOutlined>
  275. ) : (
  276. <FileIcon id={id} name={fileName}></FileIcon>
  277. )}
  278. <Flex vertical style={{ width: '90%' }}>
  279. <Text
  280. ellipsis={{ tooltip: fileName }}
  281. className={styles.nameText}
  282. >
  283. <b> {fileName}</b>
  284. </Text>
  285. {isUploadError(item) ? (
  286. t('uploadFailed')
  287. ) : (
  288. <>
  289. {item.percent !== 100 ? (
  290. t('uploading')
  291. ) : !item.response ? (
  292. t('parsing')
  293. ) : (
  294. <Space>
  295. <span>{fileExtension?.toUpperCase()},</span>
  296. <span>
  297. {formatBytes(
  298. getDocumentInfoById(id)?.size ?? 0,
  299. )}
  300. </span>
  301. </Space>
  302. )}
  303. </>
  304. )}
  305. </Flex>
  306. </Flex>
  307. {item.status !== 'uploading' && (
  308. <span className={styles.deleteIcon}>
  309. <CloseCircleOutlined onClick={() => handleRemove(item)} />
  310. </span>
  311. )}
  312. </Card>
  313. </List.Item>
  314. );
  315. }}
  316. />
  317. )}
  318. </Flex>
  319. );
  320. };
  321. export default memo(MessageInput);