From b1baa91ff00c12512666e8ab6d74c12c88b35530 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Thu, 14 Aug 2025 12:11:53 +0800 Subject: [PATCH] feat(next-search): Implements document preview functionality #3221 (#9465) ### What problem does this PR solve? feat(next-search): Implements document preview functionality - Adds a new document preview modal component - Implements document preview page logic - Adds document preview-related hooks - Optimizes document preview rendering logic ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/originui/input.tsx | 1 + web/src/components/ui/modal/modal.tsx | 2 +- web/src/components/ui/tooltip.tsx | 89 ++++++ .../document-preview/csv-preview.tsx | 6 +- .../document-preview/doc-preview.tsx | 9 +- .../document-preview/excel-preview.tsx | 5 +- .../document-preview/image-preview.tsx | 5 +- .../components/document-preview/index.tsx | 15 +- .../document-preview/pdf-preview.tsx | 6 +- .../document-preview/ppt-preview.tsx | 9 +- .../document-preview/txt-preview.tsx | 7 +- .../components/knowledge-chunk/index.tsx | 3 + .../document-preview-modal/hooks.ts | 29 ++ .../document-preview-modal/index.tsx | 65 ++++ .../next-search/highlight-markdown/index.tsx | 48 +++ web/src/pages/next-search/index.less | 8 + web/src/pages/next-search/index.tsx | 55 ++-- .../next-search/markdown-content/index.tsx | 295 ++++++++++++++++++ web/src/pages/next-search/mindmap-drawer.tsx | 47 +++ .../retrieval-documents/index.less | 11 + .../next-search/retrieval-documents/index.tsx | 237 ++++++++++++++ web/src/pages/next-search/search-home.tsx | 21 +- .../search-setting-aisummery-config.tsx | 16 +- web/src/pages/next-search/search-setting.tsx | 132 +++++--- web/src/pages/next-search/searching.tsx | 288 ++++++++++++++++- web/src/pages/next-search/spotlight.tsx | 8 +- web/src/pages/next-searches/hooks.ts | 29 +- web/src/pages/next-searches/index.tsx | 11 +- web/src/pages/search/hooks.ts | 1 + 29 files changed, 1336 insertions(+), 122 deletions(-) create mode 100644 web/src/pages/next-search/document-preview-modal/hooks.ts create mode 100644 web/src/pages/next-search/document-preview-modal/index.tsx create mode 100644 web/src/pages/next-search/highlight-markdown/index.tsx create mode 100644 web/src/pages/next-search/markdown-content/index.tsx create mode 100644 web/src/pages/next-search/mindmap-drawer.tsx create mode 100644 web/src/pages/next-search/retrieval-documents/index.less create mode 100644 web/src/pages/next-search/retrieval-documents/index.tsx diff --git a/web/src/components/originui/input.tsx b/web/src/components/originui/input.tsx index f048aa79c..dac582e3a 100644 --- a/web/src/components/originui/input.tsx +++ b/web/src/components/originui/input.tsx @@ -50,3 +50,4 @@ const Input = function ({ }; export { Input }; +export default React.forwardRef(Input); diff --git a/web/src/components/ui/modal/modal.tsx b/web/src/components/ui/modal/modal.tsx index f018b8988..9126f826a 100644 --- a/web/src/components/ui/modal/modal.tsx +++ b/web/src/components/ui/modal/modal.tsx @@ -152,7 +152,7 @@ const Modal: ModalType = ({ onClick={() => maskClosable && onOpenChange?.(false)} > e.stopPropagation()} > {/* title */} diff --git a/web/src/components/ui/tooltip.tsx b/web/src/components/ui/tooltip.tsx index 7668b4baf..3aae28d0d 100644 --- a/web/src/components/ui/tooltip.tsx +++ b/web/src/components/ui/tooltip.tsx @@ -42,3 +42,92 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => { ); }; + +export interface AntToolTipProps { + title: React.ReactNode; + children: React.ReactNode; + placement?: 'top' | 'bottom' | 'left' | 'right'; + trigger?: 'hover' | 'click' | 'focus'; + className?: string; +} + +export const AntToolTip: React.FC = ({ + title, + children, + placement = 'top', + trigger = 'hover', + className, +}) => { + const [visible, setVisible] = React.useState(false); + + const showTooltip = () => { + if (trigger === 'hover' || trigger === 'focus') { + setVisible(true); + } + }; + + const hideTooltip = () => { + if (trigger === 'hover' || trigger === 'focus') { + setVisible(false); + } + }; + + const toggleTooltip = () => { + if (trigger === 'click') { + setVisible(!visible); + } + }; + + const getPlacementClasses = () => { + switch (placement) { + case 'top': + return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2'; + case 'bottom': + return 'top-full left-1/2 transform -translate-x-1/2 mt-2'; + case 'left': + return 'right-full top-1/2 transform -translate-y-1/2 mr-2'; + case 'right': + return 'left-full top-1/2 transform -translate-y-1/2 ml-2'; + default: + return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2'; + } + }; + + return ( +
+
+ {children} +
+ {visible && title && ( +
+ {title} +
+
+ )} +
+ ); +}; diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/csv-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/csv-preview.tsx index bde425f95..45b05454e 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/csv-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/csv-preview.tsx @@ -3,7 +3,6 @@ import { Spin } from '@/components/ui/spin'; import request from '@/utils/request'; import classNames from 'classnames'; import React, { useEffect, useRef, useState } from 'react'; -import { useGetDocumentUrl } from './hooks'; interface CSVData { rows: string[][]; @@ -12,13 +11,14 @@ interface CSVData { interface FileViewerProps { className?: string; + url: string; } -const CSVFileViewer: React.FC = () => { +const CSVFileViewer: React.FC = ({ url }) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); const containerRef = useRef(null); - const url = useGetDocumentUrl(); + // const url = useGetDocumentUrl(); const parseCSV = (csvText: string): CSVData => { console.log('Parsing CSV data:', csvText); const lines = csvText.split('\n'); diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/doc-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/doc-preview.tsx index 7e4e3a8c7..845f0374e 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/doc-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/doc-preview.tsx @@ -4,14 +4,17 @@ import request from '@/utils/request'; import classNames from 'classnames'; import mammoth from 'mammoth'; import { useEffect, useState } from 'react'; -import { useGetDocumentUrl } from './hooks'; interface DocPreviewerProps { className?: string; + url: string; } -export const DocPreviewer: React.FC = ({ className }) => { - const url = useGetDocumentUrl(); +export const DocPreviewer: React.FC = ({ + className, + url, +}) => { + // const url = useGetDocumentUrl(); const [htmlContent, setHtmlContent] = useState(''); const [loading, setLoading] = useState(false); const fetchDocument = async () => { diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/excel-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/excel-preview.tsx index a6bcfcc73..c86e0462c 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/excel-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/excel-preview.tsx @@ -1,15 +1,16 @@ import { useFetchExcel } from '@/pages/document-viewer/hooks'; import classNames from 'classnames'; -import { useGetDocumentUrl } from './hooks'; interface ExcelCsvPreviewerProps { className?: string; + url: string; } export const ExcelCsvPreviewer: React.FC = ({ className, + url, }) => { - const url = useGetDocumentUrl(); + // const url = useGetDocumentUrl(); const { containerRef } = useFetchExcel(url); return ( diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/image-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/image-preview.tsx index 449cf3e7e..80e796a54 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/image-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/image-preview.tsx @@ -3,16 +3,17 @@ import { Spin } from '@/components/ui/spin'; import request from '@/utils/request'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; -import { useGetDocumentUrl } from './hooks'; interface ImagePreviewerProps { className?: string; + url: string; } export const ImagePreviewer: React.FC = ({ className, + url, }) => { - const url = useGetDocumentUrl(); + // const url = useGetDocumentUrl(); const [imageSrc, setImageSrc] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/index.tsx index 89dc50f38..dd7b35348 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/index.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/index.tsx @@ -12,12 +12,14 @@ import { TxtPreviewer } from './txt-preview'; type PreviewProps = { fileType: string; className?: string; + url: string; }; const Preview = ({ fileType, className, highlights, setWidthAndHeight, + url, }: PreviewProps & Partial) => { return ( <> @@ -26,37 +28,38 @@ const Preview = ({ )} {['doc', 'docx'].indexOf(fileType) > -1 && (
- +
)} {['txt', 'md'].indexOf(fileType) > -1 && (
- +
)} {['visual'].indexOf(fileType) > -1 && (
- +
)} {['pptx'].indexOf(fileType) > -1 && (
- +
)} {['xlsx'].indexOf(fileType) > -1 && (
- +
)} {['csv'].indexOf(fileType) > -1 && (
- +
)} diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/pdf-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/pdf-preview.tsx index 301111721..79b1c54ae 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/pdf-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/pdf-preview.tsx @@ -7,7 +7,6 @@ import { PdfLoader, Popup, } from 'react-pdf-highlighter'; -import { useGetDocumentUrl } from './hooks'; import { useCatchDocumentError } from '@/components/pdf-previewer/hooks'; import { Spin } from '@/components/ui/spin'; @@ -17,6 +16,7 @@ import styles from './index.less'; export interface IProps { highlights: IHighlight[]; setWidthAndHeight: (width: number, height: number) => void; + url: string; } const HighlightPopup = ({ comment, @@ -30,8 +30,8 @@ const HighlightPopup = ({ ) : null; // TODO: merge with DocumentPreviewer -const PdfPreview = ({ highlights: state, setWidthAndHeight }: IProps) => { - const url = useGetDocumentUrl(); +const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => { + // const url = useGetDocumentUrl(); const ref = useRef<(highlight: IHighlight) => void>(() => {}); const error = useCatchDocumentError(url); diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/ppt-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/ppt-preview.tsx index f464ae47c..7786c48c3 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/ppt-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/ppt-preview.tsx @@ -3,13 +3,16 @@ import request from '@/utils/request'; import classNames from 'classnames'; import { init } from 'pptx-preview'; import { useEffect, useRef } from 'react'; -import { useGetDocumentUrl } from './hooks'; interface PptPreviewerProps { className?: string; + url: string; } -export const PptPreviewer: React.FC = ({ className }) => { - const url = useGetDocumentUrl(); +export const PptPreviewer: React.FC = ({ + className, + url, +}) => { + // const url = useGetDocumentUrl(); const wrapper = useRef(null); const containerRef = useRef(null); const fetchDocument = async () => { diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/txt-preview.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/txt-preview.tsx index 6ae646b25..cf6649e34 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/txt-preview.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview/txt-preview.tsx @@ -3,11 +3,10 @@ import { Spin } from '@/components/ui/spin'; import request from '@/utils/request'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; -import { useGetDocumentUrl } from './hooks'; -type TxtPreviewerProps = { className?: string }; -export const TxtPreviewer = ({ className }: TxtPreviewerProps) => { - const url = useGetDocumentUrl(); +type TxtPreviewerProps = { className?: string; url: string }; +export const TxtPreviewer = ({ className, url }: TxtPreviewerProps) => { + // const url = useGetDocumentUrl(); const [loading, setLoading] = useState(false); const [data, setData] = useState(''); const fetchTxt = async () => { diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx index 79a51745d..0e4c78080 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/index.tsx @@ -40,6 +40,7 @@ import { useNavigatePage, } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; +import { useGetDocumentUrl } from '../../../knowledge-chunk/components/document-preview/hooks'; import styles from './index.less'; const Chunk = () => { @@ -73,6 +74,7 @@ const Chunk = () => { } = useUpdateChunk(); const { navigateToDataset, getQueryString, navigateToDatasetList } = useNavigatePage(); + const fileUrl = useGetDocumentUrl(); useEffect(() => { setChunkList(data); }, [data]); @@ -212,6 +214,7 @@ const Chunk = () => { fileType={fileType} highlights={highlights} setWidthAndHeight={setWidthAndHeight} + url={fileUrl} >
diff --git a/web/src/pages/next-search/document-preview-modal/hooks.ts b/web/src/pages/next-search/document-preview-modal/hooks.ts new file mode 100644 index 000000000..fe65009aa --- /dev/null +++ b/web/src/pages/next-search/document-preview-modal/hooks.ts @@ -0,0 +1,29 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import { IReferenceChunk } from '@/interfaces/database/chat'; +import { useCallback, useState } from 'react'; + +export const useClickDrawer = () => { + const { visible, showModal, hideModal } = useSetModalState(); + const [selectedChunk, setSelectedChunk] = useState( + {} as IReferenceChunk, + ); + const [documentId, setDocumentId] = useState(''); + + const clickDocumentButton = useCallback( + (documentId: string, chunk: IReferenceChunk) => { + showModal(); + setSelectedChunk(chunk); + setDocumentId(documentId); + }, + [showModal], + ); + + return { + clickDocumentButton, + visible, + showModal, + hideModal, + selectedChunk, + documentId, + }; +}; diff --git a/web/src/pages/next-search/document-preview-modal/index.tsx b/web/src/pages/next-search/document-preview-modal/index.tsx new file mode 100644 index 000000000..a803a907e --- /dev/null +++ b/web/src/pages/next-search/document-preview-modal/index.tsx @@ -0,0 +1,65 @@ +import { FileIcon } from '@/components/icon-font'; +import { Modal } from '@/components/ui/modal/modal'; +import { + useGetChunkHighlights, + useGetDocumentUrl, +} from '@/hooks/document-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { IReferenceChunk } from '@/interfaces/database/chat'; +import { IChunk } from '@/interfaces/database/knowledge'; +import DocumentPreview from '@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/document-preview'; +import { useEffect, useState } from 'react'; + +interface IProps extends IModalProps { + documentId: string; + chunk: IChunk | IReferenceChunk; +} +function getFileExtensionRegex(filename: string): string { + const match = filename.match(/\.([^.]+)$/); + return match ? match[1].toLowerCase() : ''; +} +const PdfDrawer = ({ + visible = false, + hideModal, + documentId, + chunk, +}: IProps) => { + const getDocumentUrl = useGetDocumentUrl(documentId); + const { highlights, setWidthAndHeight } = useGetChunkHighlights(chunk); + // const ref = useRef<(highlight: IHighlight) => void>(() => {}); + // const [loaded, setLoaded] = useState(false); + const url = getDocumentUrl(); + + console.log('chunk--->', chunk.docnm_kwd, url); + const [fileType, setFileType] = useState(''); + + useEffect(() => { + if (chunk.docnm_kwd) { + const type = getFileExtensionRegex(chunk.docnm_kwd); + setFileType(type); + } + }, [chunk.docnm_kwd]); + return ( + + + {chunk.docnm_kwd} + + } + onCancel={hideModal} + open={visible} + showfooter={false} + > + + + ); +}; + +export default PdfDrawer; diff --git a/web/src/pages/next-search/highlight-markdown/index.tsx b/web/src/pages/next-search/highlight-markdown/index.tsx new file mode 100644 index 000000000..304a7ed5a --- /dev/null +++ b/web/src/pages/next-search/highlight-markdown/index.tsx @@ -0,0 +1,48 @@ +import Markdown from 'react-markdown'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import rehypeKatex from 'rehype-katex'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +import remarkMath from 'remark-math'; + +import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you + +import { preprocessLaTeX } from '@/utils/chat'; + +const HightLightMarkdown = ({ + children, +}: { + children: string | null | undefined; +}) => { + return ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ); + }, + } as any + } + > + {children ? preprocessLaTeX(children) : children} + + ); +}; + +export default HightLightMarkdown; diff --git a/web/src/pages/next-search/index.less b/web/src/pages/next-search/index.less index f359d1463..dcdf21d4c 100644 --- a/web/src/pages/next-search/index.less +++ b/web/src/pages/next-search/index.less @@ -106,3 +106,11 @@ .delay-700 { animation-delay: 0.7s; } + +.highlightContent { + .multipleLineEllipsis(2); + em { + color: red; + font-style: normal; + } +} diff --git a/web/src/pages/next-search/index.tsx b/web/src/pages/next-search/index.tsx index b20b1fc3f..31870110f 100644 --- a/web/src/pages/next-search/index.tsx +++ b/web/src/pages/next-search/index.tsx @@ -10,7 +10,7 @@ import { import { Button } from '@/components/ui/button'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { Settings } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { ISearchAppDetailProps, useFetchSearchDetail, @@ -26,6 +26,13 @@ export default function SearchPage() { const { data: SearchData } = useFetchSearchDetail(); const [openSetting, setOpenSetting] = useState(false); + const [searchText, setSearchText] = useState(''); + useEffect(() => { + if (isSearching) { + setOpenSetting(false); + } + }, [isSearching]); + return (
@@ -50,6 +57,8 @@ export default function SearchPage() { )} @@ -57,33 +66,35 @@ export default function SearchPage() {
)} - {/* {openSetting && ( -
*/} - - {/*
- )} */} + {openSetting && ( + + )} -
- -
+ {!isSearching && ( +
+ +
+ )}
); } diff --git a/web/src/pages/next-search/markdown-content/index.tsx b/web/src/pages/next-search/markdown-content/index.tsx new file mode 100644 index 000000000..e03306efe --- /dev/null +++ b/web/src/pages/next-search/markdown-content/index.tsx @@ -0,0 +1,295 @@ +import Image from '@/components/image'; +import SvgIcon from '@/components/svg-icon'; +import { IReference, IReferenceChunk } from '@/interfaces/database/chat'; +import { getExtension } from '@/utils/document-util'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import DOMPurify from 'dompurify'; +import { useCallback, useEffect, useMemo } from 'react'; +import Markdown from 'react-markdown'; +import reactStringReplace from 'react-string-replace'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +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 { useFetchDocumentThumbnailsByIds } from '@/hooks/document-hooks'; +import { useTranslation } from 'react-i18next'; + +import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you + +import { + preprocessLaTeX, + replaceThinkToSection, + showImage, +} from '@/utils/chat'; + +import { Button } from '@/components/ui/button'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { currentReg, replaceTextByOldReg } from '@/pages/next-chats/utils'; +import classNames from 'classnames'; +import { omit } from 'lodash'; +import { pipe } from 'lodash/fp'; + +const getChunkIndex = (match: string) => Number(match); + +// Defining Tailwind CSS class name constants +const styles = { + referenceChunkImage: 'w-[10vw] object-contain', + referenceInnerChunkImage: 'block object-contain max-w-full max-h-[6vh]', + referenceImagePreview: 'max-w-[45vw] max-h-[45vh]', + chunkContentText: 'max-h-[45vh] overflow-y-auto', + documentLink: 'p-0', + referenceIcon: 'px-[6px]', + fileThumbnail: 'inline-block max-w-[40px]', +}; + +// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem. +const MarkdownContent = ({ + reference, + clickDocumentButton, + content, +}: { + content: string; + loading: boolean; + reference: IReference; + clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void; +}) => { + const { t } = useTranslation(); + const { setDocumentIds, data: fileThumbnails } = + useFetchDocumentThumbnailsByIds(); + const contentWithCursor = useMemo(() => { + // let text = DOMPurify.sanitize(content); + let text = content; + if (text === '') { + text = t('chat.searching'); + } + const nextText = replaceTextByOldReg(text); + return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); + }, [content, t]); + + useEffect(() => { + const docAggs = reference?.doc_aggs; + setDocumentIds(Array.isArray(docAggs) ? docAggs.map((x) => x.doc_id) : []); + }, [reference, setDocumentIds]); + + const handleDocumentButtonClick = useCallback( + ( + documentId: string, + chunk: IReferenceChunk, + isPdf: boolean, + documentUrl?: string, + ) => + () => { + if (!isPdf) { + if (!documentUrl) { + return; + } + window.open(documentUrl, '_blank'); + } else { + clickDocumentButton?.(documentId, chunk); + } + }, + [clickDocumentButton], + ); + + const rehypeWrapReference = () => { + return function wrapTextTransform(tree: any) { + visitParents(tree, 'text', (node, ancestors) => { + const latestAncestor = ancestors.at(-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 chunks = reference?.chunks ?? []; + const chunkItem = chunks[chunkIndex]; + const document = reference?.doc_aggs?.find( + (x) => x?.doc_id === chunkItem?.document_id, + ); + const documentId = document?.doc_id; + const documentUrl = document?.url; + const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; + const fileExtension = documentId ? getExtension(document?.doc_name) : ''; + const imageId = chunkItem?.image_id; + + return { + documentUrl, + fileThumbnail, + fileExtension, + imageId, + chunkItem, + documentId, + document, + }; + }, + [fileThumbnails, reference], + ); + + const getPopoverContent = useCallback( + (chunkIndex: number) => { + const { + documentUrl, + fileThumbnail, + fileExtension, + imageId, + chunkItem, + documentId, + document, + } = getReferenceInfo(chunkIndex); + + return ( +
+ {imageId && ( + + + + + + + + + )} +
+
+ {documentId && ( +
+ {fileThumbnail ? ( + + ) : ( + + )} + +
+ )} +
+
+ ); + }, + [getReferenceInfo, handleDocumentButtonClick], + ); + + const renderReference = useCallback( + (text: string) => { + let replacedText = reactStringReplace(text, currentReg, (match, i) => { + const chunkIndex = getChunkIndex(match); + + const { documentUrl, fileExtension, imageId, chunkItem, documentId } = + getReferenceInfo(chunkIndex); + + const docType = chunkItem?.doc_type; + + return showImage(docType) ? ( + {} + } + > + ) : ( + + + + + {getPopoverContent(chunkIndex)} + + ); + }); + + return replacedText; + }, + [getPopoverContent, getReferenceInfo, handleDocumentButtonClick], + ); + + return ( + + renderReference(children), + code(props: any) { + const { children, className, ...rest } = props; + const restProps = omit(rest, 'node'); + const match = /language-(\w+)/.exec(className || ''); + return match ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ); + }, + } as any + } + > + {contentWithCursor} + + ); +}; + +export default MarkdownContent; diff --git a/web/src/pages/next-search/mindmap-drawer.tsx b/web/src/pages/next-search/mindmap-drawer.tsx new file mode 100644 index 000000000..ce25e6b88 --- /dev/null +++ b/web/src/pages/next-search/mindmap-drawer.tsx @@ -0,0 +1,47 @@ +import IndentedTree from '@/components/indented-tree/indented-tree'; +import { Progress } from '@/components/ui/progress'; +import { IModalProps } from '@/interfaces/common'; +import { X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { usePendingMindMap } from '../search/hooks'; + +interface IProps extends IModalProps { + data: any; +} + +const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => { + const { t } = useTranslation(); + const percent = usePendingMindMap(); + return ( +
+
+
+ {t('chunk.mind')} +
+ { + hideModal?.(); + }} + /> +
+ {loading && ( +
+ +
+ )} + {!loading && ( +
+ +
+ )} +
+ ); +}; + +export default MindMapDrawer; diff --git a/web/src/pages/next-search/retrieval-documents/index.less b/web/src/pages/next-search/retrieval-documents/index.less new file mode 100644 index 000000000..60a5a8f6e --- /dev/null +++ b/web/src/pages/next-search/retrieval-documents/index.less @@ -0,0 +1,11 @@ +.selectFilesCollapse { + :global(.ant-collapse-header) { + padding-left: 22px; + } + margin-bottom: 32px; + overflow-y: auto; +} + +.selectFilesTitle { + padding-right: 10px; +} diff --git a/web/src/pages/next-search/retrieval-documents/index.tsx b/web/src/pages/next-search/retrieval-documents/index.tsx new file mode 100644 index 000000000..43428e37e --- /dev/null +++ b/web/src/pages/next-search/retrieval-documents/index.tsx @@ -0,0 +1,237 @@ +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command'; +import { MultiSelectOptionType } from '@/components/ui/multi-select'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + useAllTestingResult, + useSelectTestingResult, +} from '@/hooks/knowledge-hooks'; +import { cn } from '@/lib/utils'; +import { Separator } from '@radix-ui/react-select'; +import { CheckIcon, ChevronDown, Files, XIcon } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface IProps { + onTesting(documentIds: string[]): void; + setSelectedDocumentIds(documentIds: string[]): void; + selectedDocumentIds: string[]; +} + +const RetrievalDocuments = ({ + onTesting, + selectedDocumentIds, + setSelectedDocumentIds, +}: IProps) => { + const { t } = useTranslation(); + const { documents: documentsAll } = useAllTestingResult(); + const { documents } = useSelectTestingResult(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const maxCount = 3; + const { documents: useDocuments } = { + documents: + documentsAll?.length > documents?.length ? documentsAll : documents, + }; + const [selectedValues, setSelectedValues] = + useState(selectedDocumentIds); + + const multiOptions = useMemo(() => { + return useDocuments?.map((item) => { + return { + label: item.doc_name, + value: item.doc_id, + disabled: item.doc_name === 'Disabled User', + // suffix: ( + //
+ //
{item.count}
+ //
+ // + //
+ //
+ // ), + }; + }); + }, [useDocuments]); + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev); + }; + + const onValueChange = (value: string[]) => { + console.log(value); + onTesting(value); + setSelectedDocumentIds(value); + // handleDatasetSelectChange(value, field.onChange); + }; + const handleClear = () => { + setSelectedValues([]); + onValueChange([]); + }; + + const handleInputKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + setIsPopoverOpen(true); + } else if (event.key === 'Backspace' && !event.currentTarget.value) { + const newSelectedValues = [...selectedValues]; + newSelectedValues.pop(); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + } + }; + const toggleOption = (option: string) => { + const newSelectedValues = selectedValues.includes(option) + ? selectedValues.filter((value) => value !== option) + : [...selectedValues, option]; + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + return ( + + + + + setIsPopoverOpen(false)} + > + + + + No results found. + + {!multiOptions.some((x) => 'options' in x) && + (multiOptions as unknown as MultiSelectOptionType[]).map( + (option) => { + const isSelected = selectedValues.includes(option.value); + return ( + { + if (option.disabled) return false; + toggleOption(option.value); + }} + className={cn('cursor-pointer', { + 'cursor-not-allowed text-text-disabled': + option.disabled, + })} + > +
+ +
+ {option.icon && ( + + )} + + {option.label} + + {option.suffix && ( + + {option.suffix} + + )} +
+ ); + }, + )} +
+ + +
+ {selectedValues.length > 0 && ( + <> + + Clear + + + + )} + setIsPopoverOpen(false)} + className="flex-1 justify-center cursor-pointer max-w-full" + > + Close + +
+
+
+
+
+
+ ); +}; + +export default RetrievalDocuments; diff --git a/web/src/pages/next-search/search-home.tsx b/web/src/pages/next-search/search-home.tsx index 413a58553..9acae7b50 100644 --- a/web/src/pages/next-search/search-home.tsx +++ b/web/src/pages/next-search/search-home.tsx @@ -1,5 +1,6 @@ import { Input } from '@/components/originui/input'; import { Button } from '@/components/ui/button'; +import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; import { cn } from '@/lib/utils'; import { Search } from 'lucide-react'; import { Dispatch, SetStateAction } from 'react'; @@ -9,10 +10,15 @@ import Spotlight from './spotlight'; export default function SearchPage({ isSearching, setIsSearching, + searchText, + setSearchText, }: { isSearching: boolean; setIsSearching: Dispatch>; + searchText: string; + setSearchText: Dispatch>; }) { + const { data: userInfo } = useFetchUserInfo(); return (
@@ -30,14 +36,25 @@ export default function SearchPage({ {!isSearching && ( <>

👋 Hi there

-

Welcome back, KiKi

+

+ Welcome back, {userInfo?.nickname} +

)}
{ + if (e.key === 'Enter') { + setIsSearching(!isSearching); + } + }} + onChange={(e) => { + setSearchText(e.target.value || ''); + }} />
+ + {/* search body */} +
+ {searchData.search_config.summary && ( + <> +
+ AI Summary +
+ {isEmpty(answer) && sendingLoading ? ( +
+ + + +
+ ) : ( + answer.answer && ( +
+ +
+ ) + )} + + )} + +
+ {/* retrieval documents */} +
+ +
+
+
+ + {chunks?.length > 0 && ( + <> + {chunks.map((chunk, index) => { + return ( + <> +
+
+ + + +
+
+ + + {chunk.content_with_weight} + + +
+
+
+ clickDocumentButton(chunk.doc_id, chunk as any) + } + > + + {chunk.docnm_kwd} +
+
+ {index < chunks.length - 1 && ( +
+ )} + + ); + })} + + )} +
+ {relatedQuestions?.length > 0 && ( +
+
+ {relatedQuestions?.map((x, idx) => ( + + {x} + + ))} +
+
+ )} +
+
+
+ +
+ {!mindMapVisible && + !isFirstRender && + !isSearchStrEmpty && + !isEmpty(searchData.search_config.kb_ids) && ( + + + + + {t('chunk.mind')} + + )} + {visible && ( + + )} + {mindMapVisible && ( +
+ +
+ )}
); } diff --git a/web/src/pages/next-search/spotlight.tsx b/web/src/pages/next-search/spotlight.tsx index 1e17b8e48..2e1e3dc85 100644 --- a/web/src/pages/next-search/spotlight.tsx +++ b/web/src/pages/next-search/spotlight.tsx @@ -1,3 +1,4 @@ +import { useIsDarkTheme } from '@/components/theme-provider'; import React from 'react'; interface SpotlightProps { @@ -5,6 +6,8 @@ interface SpotlightProps { } const Spotlight: React.FC = ({ className }) => { + const isDark = useIsDarkTheme(); + console.log('isDark', isDark); return (
= ({ className }) => {
diff --git a/web/src/pages/next-searches/hooks.ts b/web/src/pages/next-searches/hooks.ts index 98eff23aa..a38720561 100644 --- a/web/src/pages/next-searches/hooks.ts +++ b/web/src/pages/next-searches/hooks.ts @@ -1,8 +1,8 @@ // src/pages/next-searches/hooks.ts +import message from '@/components/ui/message'; import searchService from '@/services/search-service'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { message } from 'antd'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'umi'; @@ -23,7 +23,6 @@ export const useCreateSearch = () => { const { data, - isLoading, isError, mutateAsync: createSearchMutation, } = useMutation({ @@ -50,7 +49,7 @@ export const useCreateSearch = () => { [createSearchMutation], ); - return { data, isLoading, isError, createSearch }; + return { data, isError, createSearch }; }; export interface SearchListParams { @@ -128,7 +127,6 @@ export const useDeleteSearch = () => { const { data, - isLoading, isError, mutateAsync: deleteSearchMutation, } = useMutation({ @@ -155,7 +153,7 @@ export const useDeleteSearch = () => { [deleteSearchMutation], ); - return { data, isLoading, isError, deleteSearch }; + return { data, isError, deleteSearch }; }; interface IllmSettingProps { @@ -166,7 +164,12 @@ interface IllmSettingProps { frequency_penalty: number; presence_penalty: number; } - +interface IllmSettingEnableProps { + temperatureEnabled?: boolean; + topPEnabled?: boolean; + presencePenaltyEnabled?: boolean; + frequencyPenaltyEnabled?: boolean; +} export interface ISearchAppDetailProps { avatar: any; created_by: string; @@ -184,7 +187,7 @@ export interface ISearchAppDetailProps { rerank_id: string; similarity_threshold: number; summary: boolean; - llm_setting: IllmSettingProps; + llm_setting: IllmSettingProps & IllmSettingEnableProps; top_k: number; use_kg: boolean; vector_similarity_weight: number; @@ -225,10 +228,9 @@ export type IUpdateSearchProps = Omit & { export const useUpdateSearch = () => { const { t } = useTranslation(); - + const queryClient = useQueryClient(); const { data, - isLoading, isError, mutateAsync: updateSearchMutation, } = useMutation({ @@ -241,8 +243,11 @@ export const useUpdateSearch = () => { } return response.data; }, - onSuccess: () => { + onSuccess: (data, variables) => { message.success(t('message.updated')); + queryClient.invalidateQueries({ + queryKey: ['searchDetail', variables.search_id], + }); }, onError: (error) => { message.error(t('message.error', { error: error.message })); @@ -256,5 +261,5 @@ export const useUpdateSearch = () => { [updateSearchMutation], ); - return { data, isLoading, isError, updateSearch }; + return { data, isError, updateSearch }; }; diff --git a/web/src/pages/next-searches/index.tsx b/web/src/pages/next-searches/index.tsx index 3f7828c2a..453af7aff 100644 --- a/web/src/pages/next-searches/index.tsx +++ b/web/src/pages/next-searches/index.tsx @@ -12,6 +12,7 @@ import { import { Modal } from '@/components/ui/modal/modal'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { useTranslate } from '@/hooks/common-hooks'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { zodResolver } from '@hookform/resolvers/zod'; import { pick } from 'lodash'; import { Plus, Search } from 'lucide-react'; @@ -30,6 +31,7 @@ type SearchFormValues = z.infer; export default function SearchList() { // const { data } = useFetchFlowList(); const { t } = useTranslate('search'); + const { navigateToSearch } = useNavigatePage(); const { isLoading, createSearch } = useCreateSearch(); const { data: list, @@ -48,7 +50,10 @@ export default function SearchList() { }; const onSubmit = async (values: SearchFormValues) => { - await createSearch({ name: values.name }); + const res = await createSearch({ name: values.name }); + if (res) { + navigateToSearch(res?.search_id); + } if (!isLoading) { setOpenCreateModal(false); } @@ -88,16 +93,12 @@ export default function SearchList() { {list?.data.search_apps.map((x) => { return ; })} - {/* {data.map((x) => { - return ; - })} */}
{list?.data.total && ( )} { answer: currentAnswer, relatedQuestions: relatedQuestions?.slice(0, 5) ?? [], searchStr, + setSearchStr, isFirstRender, selectedDocumentIds, isSearchStrEmpty: isEmpty(trim(searchStr)),