import Image from '@/components/image'; import SvgIcon from '@/components/svg-icon'; import { useFetchDocumentThumbnailsByIds, useGetDocumentUrl, } from '@/hooks/use-document-request'; import { IReference, IReferenceChunk } from '@/interfaces/database/chat'; import { currentReg, preprocessLaTeX, replaceTextByOldReg, replaceThinkToSection, showImage, } from '@/utils/chat'; import { getExtension } from '@/utils/document-util'; import { InfoCircleOutlined } from '@ant-design/icons'; import { Button, Flex, Popover, Tooltip } from 'antd'; import classNames from 'classnames'; import DOMPurify from 'dompurify'; import 'katex/dist/katex.min.css'; import { omit } from 'lodash'; import { pipe } from 'lodash/fp'; import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import Markdown from 'react-markdown'; import reactStringReplace from 'react-string-replace'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { oneDark, oneLight, } from 'react-syntax-highlighter/dist/esm/styles/prism'; import rehypeKatex from 'rehype-katex'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import { visitParents } from 'unist-util-visit-parents'; import styles from './floating-chat-widget-markdown.less'; import { useIsDarkTheme } from './theme-provider'; const getChunkIndex = (match: string) => Number(match.replace(/\[|\]/g, '')); const FloatingChatWidgetMarkdown = ({ reference, clickDocumentButton, content, }: { content: string; loading: boolean; reference: IReference; clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void; }) => { const { t } = useTranslation(); const { setDocumentIds, data: fileThumbnails } = useFetchDocumentThumbnailsByIds(); const getDocumentUrl = useGetDocumentUrl(); const isDarkTheme = useIsDarkTheme(); const contentWithCursor = useMemo(() => { let text = content === '' ? t('chat.searching') : content; const nextText = replaceTextByOldReg(text); return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); }, [content, t]); useEffect(() => { const docAggs = reference?.doc_aggs; const docList = Array.isArray(docAggs) ? docAggs : Object.values(docAggs ?? {}); setDocumentIds(docList.map((x: any) => x.doc_id).filter(Boolean)); }, [reference, setDocumentIds]); const handleDocumentButtonClick = useCallback( ( documentId: string, chunk: IReferenceChunk, isPdf: boolean, documentUrl?: string, ) => () => { if (!documentId) return; if (!isPdf && documentUrl) { window.open(documentUrl, '_blank'); } else if (clickDocumentButton) { clickDocumentButton(documentId, chunk); } }, [clickDocumentButton], ); const rehypeWrapReference = () => (tree: any) => { visitParents(tree, 'text', (node, ancestors) => { const latestAncestor = ancestors[ancestors.length - 1]; if ( latestAncestor.tagName !== 'custom-typography' && latestAncestor.tagName !== 'code' ) { node.type = 'element'; node.tagName = 'custom-typography'; node.properties = {}; node.children = [{ type: 'text', value: node.value }]; } }); }; const getReferenceInfo = useCallback( (chunkIndex: number) => { const chunkItem = reference?.chunks?.[chunkIndex]; if (!chunkItem) return null; const docAggsArray = Array.isArray(reference?.doc_aggs) ? reference.doc_aggs : Object.values(reference?.doc_aggs ?? {}); const document = docAggsArray.find( (x: any) => x?.doc_id === chunkItem?.document_id, ) as any; const documentId = document?.doc_id; const documentUrl = document?.url ?? (documentId ? getDocumentUrl(documentId) : undefined); const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; const fileExtension = documentId ? getExtension(document?.doc_name ?? '') : ''; return { documentUrl, fileThumbnail, fileExtension, imageId: chunkItem.image_id, chunkItem, documentId, document, }; }, [fileThumbnails, reference, getDocumentUrl], ); const getPopoverContent = useCallback( (chunkIndex: number) => { const info = getReferenceInfo(chunkIndex); if (!info) { return (
{children}
);
},
} as any
}
>
{contentWithCursor}