### 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)tags/v0.20.2
| @@ -50,3 +50,4 @@ const Input = function ({ | |||
| }; | |||
| export { Input }; | |||
| export default React.forwardRef(Input); | |||
| @@ -152,7 +152,7 @@ const Modal: ModalType = ({ | |||
| onClick={() => maskClosable && onOpenChange?.(false)} | |||
| > | |||
| <DialogPrimitive.Content | |||
| className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg transition-all focus-visible:!outline-none`} | |||
| className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg border transition-all focus-visible:!outline-none`} | |||
| onClick={(e) => e.stopPropagation()} | |||
| > | |||
| {/* title */} | |||
| @@ -42,3 +42,92 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => { | |||
| </Tooltip> | |||
| ); | |||
| }; | |||
| 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<AntToolTipProps> = ({ | |||
| 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 ( | |||
| <div className="inline-block relative"> | |||
| <div | |||
| onMouseEnter={showTooltip} | |||
| onMouseLeave={hideTooltip} | |||
| onClick={toggleTooltip} | |||
| onFocus={showTooltip} | |||
| onBlur={hideTooltip} | |||
| > | |||
| {children} | |||
| </div> | |||
| {visible && title && ( | |||
| <div | |||
| className={cn( | |||
| 'absolute z-50 px-2.5 py-1.5 text-xs text-white bg-gray-800 rounded-sm shadow-sm whitespace-nowrap', | |||
| getPlacementClasses(), | |||
| className, | |||
| )} | |||
| > | |||
| {title} | |||
| <div | |||
| className={cn( | |||
| 'absolute w-2 h-2 bg-gray-800', | |||
| placement === 'top' && | |||
| 'bottom-[-4px] left-1/2 transform -translate-x-1/2 rotate-45', | |||
| placement === 'bottom' && | |||
| 'top-[-4px] left-1/2 transform -translate-x-1/2 rotate-45', | |||
| placement === 'left' && | |||
| 'right-[-4px] top-1/2 transform -translate-y-1/2 rotate-45', | |||
| placement === 'right' && | |||
| 'left-[-4px] top-1/2 transform -translate-y-1/2 rotate-45', | |||
| )} | |||
| /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| }; | |||
| @@ -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<FileViewerProps> = () => { | |||
| const CSVFileViewer: React.FC<FileViewerProps> = ({ url }) => { | |||
| const [data, setData] = useState<CSVData | null>(null); | |||
| const [isLoading, setIsLoading] = useState<boolean>(true); | |||
| const containerRef = useRef<HTMLDivElement>(null); | |||
| const url = useGetDocumentUrl(); | |||
| // const url = useGetDocumentUrl(); | |||
| const parseCSV = (csvText: string): CSVData => { | |||
| console.log('Parsing CSV data:', csvText); | |||
| const lines = csvText.split('\n'); | |||
| @@ -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<DocPreviewerProps> = ({ className }) => { | |||
| const url = useGetDocumentUrl(); | |||
| export const DocPreviewer: React.FC<DocPreviewerProps> = ({ | |||
| className, | |||
| url, | |||
| }) => { | |||
| // const url = useGetDocumentUrl(); | |||
| const [htmlContent, setHtmlContent] = useState<string>(''); | |||
| const [loading, setLoading] = useState(false); | |||
| const fetchDocument = async () => { | |||
| @@ -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<ExcelCsvPreviewerProps> = ({ | |||
| className, | |||
| url, | |||
| }) => { | |||
| const url = useGetDocumentUrl(); | |||
| // const url = useGetDocumentUrl(); | |||
| const { containerRef } = useFetchExcel(url); | |||
| return ( | |||
| @@ -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<ImagePreviewerProps> = ({ | |||
| className, | |||
| url, | |||
| }) => { | |||
| const url = useGetDocumentUrl(); | |||
| // const url = useGetDocumentUrl(); | |||
| const [imageSrc, setImageSrc] = useState<string | null>(null); | |||
| const [isLoading, setIsLoading] = useState<boolean>(true); | |||
| @@ -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<IProps>) => { | |||
| return ( | |||
| <> | |||
| @@ -26,37 +28,38 @@ const Preview = ({ | |||
| <PdfPreviewer | |||
| highlights={highlights} | |||
| setWidthAndHeight={setWidthAndHeight} | |||
| url={url} | |||
| ></PdfPreviewer> | |||
| </section> | |||
| )} | |||
| {['doc', 'docx'].indexOf(fileType) > -1 && ( | |||
| <section> | |||
| <DocPreviewer className={className} /> | |||
| <DocPreviewer className={className} url={url} /> | |||
| </section> | |||
| )} | |||
| {['txt', 'md'].indexOf(fileType) > -1 && ( | |||
| <section> | |||
| <TxtPreviewer className={className} /> | |||
| <TxtPreviewer className={className} url={url} /> | |||
| </section> | |||
| )} | |||
| {['visual'].indexOf(fileType) > -1 && ( | |||
| <section> | |||
| <ImagePreviewer className={className} /> | |||
| <ImagePreviewer className={className} url={url} /> | |||
| </section> | |||
| )} | |||
| {['pptx'].indexOf(fileType) > -1 && ( | |||
| <section> | |||
| <PptPreviewer className={className} /> | |||
| <PptPreviewer className={className} url={url} /> | |||
| </section> | |||
| )} | |||
| {['xlsx'].indexOf(fileType) > -1 && ( | |||
| <section> | |||
| <ExcelCsvPreviewer className={className} /> | |||
| <ExcelCsvPreviewer className={className} url={url} /> | |||
| </section> | |||
| )} | |||
| {['csv'].indexOf(fileType) > -1 && ( | |||
| <section> | |||
| <CSVFileViewer className={className} /> | |||
| <CSVFileViewer className={className} url={url} /> | |||
| </section> | |||
| )} | |||
| </> | |||
| @@ -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); | |||
| @@ -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<PptPreviewerProps> = ({ className }) => { | |||
| const url = useGetDocumentUrl(); | |||
| export const PptPreviewer: React.FC<PptPreviewerProps> = ({ | |||
| className, | |||
| url, | |||
| }) => { | |||
| // const url = useGetDocumentUrl(); | |||
| const wrapper = useRef<HTMLDivElement>(null); | |||
| const containerRef = useRef<HTMLDivElement>(null); | |||
| const fetchDocument = async () => { | |||
| @@ -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<string>(''); | |||
| const fetchTxt = async () => { | |||
| @@ -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} | |||
| ></DocumentPreview> | |||
| </section> | |||
| </div> | |||
| @@ -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<IReferenceChunk>( | |||
| {} as IReferenceChunk, | |||
| ); | |||
| const [documentId, setDocumentId] = useState<string>(''); | |||
| const clickDocumentButton = useCallback( | |||
| (documentId: string, chunk: IReferenceChunk) => { | |||
| showModal(); | |||
| setSelectedChunk(chunk); | |||
| setDocumentId(documentId); | |||
| }, | |||
| [showModal], | |||
| ); | |||
| return { | |||
| clickDocumentButton, | |||
| visible, | |||
| showModal, | |||
| hideModal, | |||
| selectedChunk, | |||
| documentId, | |||
| }; | |||
| }; | |||
| @@ -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<any> { | |||
| 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 ( | |||
| <Modal | |||
| title={ | |||
| <div className="flex items-center gap-2"> | |||
| <FileIcon name={chunk.docnm_kwd}></FileIcon> | |||
| {chunk.docnm_kwd} | |||
| </div> | |||
| } | |||
| onCancel={hideModal} | |||
| open={visible} | |||
| showfooter={false} | |||
| > | |||
| <DocumentPreview | |||
| className={'!h-[calc(100dvh-300px)] overflow-auto'} | |||
| fileType={fileType} | |||
| highlights={highlights} | |||
| setWidthAndHeight={setWidthAndHeight} | |||
| url={url} | |||
| ></DocumentPreview> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default PdfDrawer; | |||
| @@ -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 ( | |||
| <Markdown | |||
| remarkPlugins={[remarkGfm, remarkMath]} | |||
| rehypePlugins={[rehypeRaw, rehypeKatex]} | |||
| className="text-text-primary text-sm" | |||
| components={ | |||
| { | |||
| code(props: any) { | |||
| const { children, className, ...rest } = props; | |||
| const match = /language-(\w+)/.exec(className || ''); | |||
| return match ? ( | |||
| <SyntaxHighlighter {...rest} PreTag="div" language={match[1]}> | |||
| {String(children).replace(/\n$/, '')} | |||
| </SyntaxHighlighter> | |||
| ) : ( | |||
| <code | |||
| {...rest} | |||
| className={`${className} pt-1 px-2 pb-2 m-0 whitespace-break-spaces rounded text-text-primary text-sm`} | |||
| > | |||
| {children} | |||
| </code> | |||
| ); | |||
| }, | |||
| } as any | |||
| } | |||
| > | |||
| {children ? preprocessLaTeX(children) : children} | |||
| </Markdown> | |||
| ); | |||
| }; | |||
| export default HightLightMarkdown; | |||
| @@ -106,3 +106,11 @@ | |||
| .delay-700 { | |||
| animation-delay: 0.7s; | |||
| } | |||
| .highlightContent { | |||
| .multipleLineEllipsis(2); | |||
| em { | |||
| color: red; | |||
| font-style: normal; | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <section> | |||
| <PageHeader> | |||
| @@ -50,6 +57,8 @@ export default function SearchPage() { | |||
| <SearchHome | |||
| setIsSearching={setIsSearching} | |||
| isSearching={isSearching} | |||
| searchText={searchText} | |||
| setSearchText={setSearchText} | |||
| /> | |||
| </div> | |||
| )} | |||
| @@ -57,33 +66,35 @@ export default function SearchPage() { | |||
| <div className="animate-fade-in-up"> | |||
| <SearchingPage | |||
| setIsSearching={setIsSearching} | |||
| isSearching={isSearching} | |||
| searchText={searchText} | |||
| setSearchText={setSearchText} | |||
| data={SearchData as ISearchAppDetailProps} | |||
| /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| {/* {openSetting && ( | |||
| <div className=" w-[440px]"> */} | |||
| <SearchSetting | |||
| className="mt-20 mr-2" | |||
| open={openSetting} | |||
| setOpen={setOpenSetting} | |||
| data={SearchData as ISearchAppDetailProps} | |||
| /> | |||
| {/* </div> | |||
| )} */} | |||
| {openSetting && ( | |||
| <SearchSetting | |||
| className="mt-20 mr-2" | |||
| open={openSetting} | |||
| setOpen={setOpenSetting} | |||
| data={SearchData as ISearchAppDetailProps} | |||
| /> | |||
| )} | |||
| </div> | |||
| <div className="absolute left-5 bottom-12 "> | |||
| <Button | |||
| variant="transparent" | |||
| className="bg-bg-card" | |||
| onClick={() => setOpenSetting(!openSetting)} | |||
| > | |||
| <Settings className="text-text-secondary" /> | |||
| <div className="text-text-secondary">Search Settings</div> | |||
| </Button> | |||
| </div> | |||
| {!isSearching && ( | |||
| <div className="absolute left-5 bottom-12 "> | |||
| <Button | |||
| variant="transparent" | |||
| className="bg-bg-card" | |||
| onClick={() => setOpenSetting(!openSetting)} | |||
| > | |||
| <Settings className="text-text-secondary" /> | |||
| <div className="text-text-secondary">Search Settings</div> | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -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 ( | |||
| <div key={chunkItem?.id} className="flex gap-2"> | |||
| {imageId && ( | |||
| <Popover> | |||
| <PopoverTrigger> | |||
| <Image | |||
| id={imageId} | |||
| className={styles.referenceChunkImage} | |||
| ></Image> | |||
| </PopoverTrigger> | |||
| <PopoverContent> | |||
| <Image | |||
| id={imageId} | |||
| className={styles.referenceImagePreview} | |||
| ></Image> | |||
| </PopoverContent> | |||
| </Popover> | |||
| )} | |||
| <div className={'space-y-2 max-w-[40vw]'}> | |||
| <div | |||
| dangerouslySetInnerHTML={{ | |||
| __html: DOMPurify.sanitize(chunkItem?.content ?? ''), | |||
| }} | |||
| className={classNames(styles.chunkContentText)} | |||
| ></div> | |||
| {documentId && ( | |||
| <div className="flex gap-2"> | |||
| {fileThumbnail ? ( | |||
| <img | |||
| src={fileThumbnail} | |||
| alt="" | |||
| className={styles.fileThumbnail} | |||
| /> | |||
| ) : ( | |||
| <SvgIcon | |||
| name={`file-icon/${fileExtension}`} | |||
| width={24} | |||
| ></SvgIcon> | |||
| )} | |||
| <Button | |||
| variant="link" | |||
| className={classNames(styles.documentLink, 'text-wrap')} | |||
| onClick={handleDocumentButtonClick( | |||
| documentId, | |||
| chunkItem, | |||
| fileExtension === 'pdf', | |||
| documentUrl, | |||
| )} | |||
| > | |||
| {document?.doc_name} | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ); | |||
| }, | |||
| [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) ? ( | |||
| <Image | |||
| id={imageId} | |||
| className={styles.referenceInnerChunkImage} | |||
| onClick={ | |||
| documentId | |||
| ? handleDocumentButtonClick( | |||
| documentId, | |||
| chunkItem, | |||
| fileExtension === 'pdf', | |||
| documentUrl, | |||
| ) | |||
| : () => {} | |||
| } | |||
| ></Image> | |||
| ) : ( | |||
| <Popover> | |||
| <PopoverTrigger> | |||
| <InfoCircleOutlined className={styles.referenceIcon} /> | |||
| </PopoverTrigger> | |||
| <PopoverContent>{getPopoverContent(chunkIndex)}</PopoverContent> | |||
| </Popover> | |||
| ); | |||
| }); | |||
| return replacedText; | |||
| }, | |||
| [getPopoverContent, getReferenceInfo, handleDocumentButtonClick], | |||
| ); | |||
| return ( | |||
| <Markdown | |||
| rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]} | |||
| remarkPlugins={[remarkGfm, remarkMath]} | |||
| className="[&>section.think]:pl-[10px] [&>section.think]:text-[#8b8b8b] [&>section.think]:border-l-2 [&>section.think]:border-l-[#d5d3d3] [&>section.think]:mb-[10px] [&>section.think]:text-xs [&>blockquote]:pl-[10px] [&>blockquote]:border-l-4 [&>blockquote]:border-l-[#ccc] text-sm" | |||
| components={ | |||
| { | |||
| 'custom-typography': ({ children }: { children: string }) => | |||
| renderReference(children), | |||
| code(props: any) { | |||
| const { children, className, ...rest } = props; | |||
| const restProps = omit(rest, 'node'); | |||
| const match = /language-(\w+)/.exec(className || ''); | |||
| return match ? ( | |||
| <SyntaxHighlighter | |||
| {...restProps} | |||
| PreTag="div" | |||
| language={match[1]} | |||
| wrapLongLines | |||
| > | |||
| {String(children).replace(/\n$/, '')} | |||
| </SyntaxHighlighter> | |||
| ) : ( | |||
| <code | |||
| {...restProps} | |||
| className={classNames(className, 'text-wrap')} | |||
| > | |||
| {children} | |||
| </code> | |||
| ); | |||
| }, | |||
| } as any | |||
| } | |||
| > | |||
| {contentWithCursor} | |||
| </Markdown> | |||
| ); | |||
| }; | |||
| export default MarkdownContent; | |||
| @@ -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<any> { | |||
| data: any; | |||
| } | |||
| const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => { | |||
| const { t } = useTranslation(); | |||
| const percent = usePendingMindMap(); | |||
| return ( | |||
| <div className="w-[400px] h-[420px]"> | |||
| <div className="flex w-full justify-between items-center mb-2"> | |||
| <div className="text-text-primary font-medium text-base"> | |||
| {t('chunk.mind')} | |||
| </div> | |||
| <X | |||
| className="text-text-primary cursor-pointer" | |||
| size={16} | |||
| onClick={() => { | |||
| hideModal?.(); | |||
| }} | |||
| /> | |||
| </div> | |||
| {loading && ( | |||
| <div className="absolute top-48"> | |||
| <Progress value={percent} className="h-1 flex-1 min-w-10" /> | |||
| </div> | |||
| )} | |||
| {!loading && ( | |||
| <div className="bg-bg-card rounded-lg p-4 w-[400px] h-[380px]"> | |||
| <IndentedTree | |||
| data={data} | |||
| show | |||
| style={{ width: '100%', height: '100%' }} | |||
| ></IndentedTree> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| }; | |||
| export default MindMapDrawer; | |||
| @@ -0,0 +1,11 @@ | |||
| .selectFilesCollapse { | |||
| :global(.ant-collapse-header) { | |||
| padding-left: 22px; | |||
| } | |||
| margin-bottom: 32px; | |||
| overflow-y: auto; | |||
| } | |||
| .selectFilesTitle { | |||
| padding-right: 10px; | |||
| } | |||
| @@ -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<string[]>(selectedDocumentIds); | |||
| const multiOptions = useMemo(() => { | |||
| return useDocuments?.map((item) => { | |||
| return { | |||
| label: item.doc_name, | |||
| value: item.doc_id, | |||
| disabled: item.doc_name === 'Disabled User', | |||
| // suffix: ( | |||
| // <div className="flex justify-between gap-3 "> | |||
| // <div>{item.count}</div> | |||
| // <div> | |||
| // <Eye /> | |||
| // </div> | |||
| // </div> | |||
| // ), | |||
| }; | |||
| }); | |||
| }, [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<HTMLInputElement>) => { | |||
| 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 ( | |||
| <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> | |||
| <PopoverTrigger asChild> | |||
| <Button | |||
| onClick={handleTogglePopover} | |||
| className={cn( | |||
| 'flex w-full p-1 rounded-md text-base text-text-primary border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto', | |||
| )} | |||
| > | |||
| <div className="flex justify-between items-center w-full"> | |||
| <div className="flex flex-wrap items-center gap-2"> | |||
| <Files /> | |||
| <span> | |||
| {selectedDocumentIds?.length ?? 0}/{useDocuments?.length ?? 0} | |||
| </span> | |||
| Files | |||
| </div> | |||
| <div className="flex items-center justify-between"> | |||
| <XIcon | |||
| className="h-4 mx-2 cursor-pointer text-muted-foreground" | |||
| onClick={(event) => { | |||
| event.stopPropagation(); | |||
| handleClear(); | |||
| }} | |||
| /> | |||
| <Separator | |||
| orientation="vertical" | |||
| className="flex min-h-6 h-full" | |||
| /> | |||
| <ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" /> | |||
| </div> | |||
| </div> | |||
| </Button> | |||
| </PopoverTrigger> | |||
| <PopoverContent | |||
| className="w-auto p-0" | |||
| align="start" | |||
| onEscapeKeyDown={() => setIsPopoverOpen(false)} | |||
| > | |||
| <Command> | |||
| <CommandInput | |||
| placeholder="Search..." | |||
| onKeyDown={handleInputKeyDown} | |||
| /> | |||
| <CommandList> | |||
| <CommandEmpty>No results found.</CommandEmpty> | |||
| <CommandGroup> | |||
| {!multiOptions.some((x) => 'options' in x) && | |||
| (multiOptions as unknown as MultiSelectOptionType[]).map( | |||
| (option) => { | |||
| const isSelected = selectedValues.includes(option.value); | |||
| return ( | |||
| <CommandItem | |||
| key={option.value} | |||
| onSelect={() => { | |||
| if (option.disabled) return false; | |||
| toggleOption(option.value); | |||
| }} | |||
| className={cn('cursor-pointer', { | |||
| 'cursor-not-allowed text-text-disabled': | |||
| option.disabled, | |||
| })} | |||
| > | |||
| <div | |||
| className={cn( | |||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
| isSelected | |||
| ? 'bg-primary ' | |||
| : 'opacity-50 [&_svg]:invisible', | |||
| { 'text-primary-foreground': !option.disabled }, | |||
| { 'text-text-disabled': option.disabled }, | |||
| )} | |||
| > | |||
| <CheckIcon className="h-4 w-4" /> | |||
| </div> | |||
| {option.icon && ( | |||
| <option.icon | |||
| className={cn('mr-2 h-4 w-4 ', { | |||
| 'text-text-disabled': option.disabled, | |||
| 'text-muted-foreground': !option.disabled, | |||
| })} | |||
| /> | |||
| )} | |||
| <span | |||
| className={cn({ | |||
| 'text-text-disabled': option.disabled, | |||
| })} | |||
| > | |||
| {option.label} | |||
| </span> | |||
| {option.suffix && ( | |||
| <span | |||
| className={cn({ | |||
| 'text-text-disabled': option.disabled, | |||
| })} | |||
| > | |||
| {option.suffix} | |||
| </span> | |||
| )} | |||
| </CommandItem> | |||
| ); | |||
| }, | |||
| )} | |||
| </CommandGroup> | |||
| <CommandSeparator /> | |||
| <CommandGroup> | |||
| <div className="flex items-center justify-between"> | |||
| {selectedValues.length > 0 && ( | |||
| <> | |||
| <CommandItem | |||
| onSelect={handleClear} | |||
| className="flex-1 justify-center cursor-pointer" | |||
| > | |||
| Clear | |||
| </CommandItem> | |||
| <Separator | |||
| orientation="vertical" | |||
| className="flex min-h-6 h-full" | |||
| /> | |||
| </> | |||
| )} | |||
| <CommandItem | |||
| onSelect={() => setIsPopoverOpen(false)} | |||
| className="flex-1 justify-center cursor-pointer max-w-full" | |||
| > | |||
| Close | |||
| </CommandItem> | |||
| </div> | |||
| </CommandGroup> | |||
| </CommandList> | |||
| </Command> | |||
| </PopoverContent> | |||
| </Popover> | |||
| ); | |||
| }; | |||
| export default RetrievalDocuments; | |||
| @@ -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<SetStateAction<boolean>>; | |||
| searchText: string; | |||
| setSearchText: Dispatch<SetStateAction<string>>; | |||
| }) { | |||
| const { data: userInfo } = useFetchUserInfo(); | |||
| return ( | |||
| <section className="relative w-full flex transition-all justify-center items-center mt-32"> | |||
| <div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]"> | |||
| @@ -30,14 +36,25 @@ export default function SearchPage({ | |||
| {!isSearching && ( | |||
| <> | |||
| <p className="mb-4 transition-opacity">👋 Hi there</p> | |||
| <p className="mb-10 transition-opacity">Welcome back, KiKi</p> | |||
| <p className="mb-10 transition-opacity"> | |||
| Welcome back, {userInfo?.nickname} | |||
| </p> | |||
| </> | |||
| )} | |||
| <div className="relative w-full "> | |||
| <Input | |||
| placeholder="How can I help you today?" | |||
| className="w-full rounded-full py-6 px-4 pr-10 text-white text-lg bg-background delay-700" | |||
| className="w-full rounded-full py-6 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700" | |||
| value={searchText} | |||
| onKeyUp={(e) => { | |||
| if (e.key === 'Enter') { | |||
| setIsSearching(!isSearching); | |||
| } | |||
| }} | |||
| onChange={(e) => { | |||
| setSearchText(e.target.value || ''); | |||
| }} | |||
| /> | |||
| <button | |||
| type="button" | |||
| @@ -33,15 +33,16 @@ interface LlmSettingFieldItemsProps { | |||
| export const LlmSettingSchema = { | |||
| llm_id: z.string(), | |||
| parameter: z.string(), | |||
| temperature: z.coerce.number(), | |||
| top_p: z.string(), | |||
| top_p: z.coerce.number(), | |||
| presence_penalty: z.coerce.number(), | |||
| frequency_penalty: z.coerce.number(), | |||
| temperatureEnabled: z.boolean(), | |||
| topPEnabled: z.boolean(), | |||
| presencePenaltyEnabled: z.boolean(), | |||
| frequencyPenaltyEnabled: z.boolean(), | |||
| maxTokensEnabled: z.boolean(), | |||
| // maxTokensEnabled: z.boolean(), | |||
| }; | |||
| export function LlmSettingFieldItems({ | |||
| @@ -58,7 +59,8 @@ export function LlmSettingFieldItems({ | |||
| const handleChange = useCallback( | |||
| (parameter: string) => { | |||
| // const currentValues = { ...form.getValues() }; | |||
| const currentValues = { ...form.getValues() }; | |||
| console.log('currentValues', currentValues); | |||
| const values = | |||
| settledModelVariableMap[ | |||
| parameter as keyof typeof settledModelVariableMap | |||
| @@ -145,28 +147,28 @@ export function LlmSettingFieldItems({ | |||
| /> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('temperature')} | |||
| checkName="temperatureEnabled" | |||
| checkName={getFieldWithPrefix('temperatureEnabled')} | |||
| label="temperature" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('top_p')} | |||
| checkName="topPEnabled" | |||
| checkName={getFieldWithPrefix('topPEnabled')} | |||
| label="topP" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('presence_penalty')} | |||
| checkName="presencePenaltyEnabled" | |||
| checkName={getFieldWithPrefix('presencePenaltyEnabled')} | |||
| label="presencePenalty" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('frequency_penalty')} | |||
| checkName="frequencyPenaltyEnabled" | |||
| checkName={getFieldWithPrefix('frequencyPenaltyEnabled')} | |||
| label="frequencyPenalty" | |||
| max={1} | |||
| step={0.01} | |||
| @@ -30,17 +30,24 @@ import { cn } from '@/lib/utils'; | |||
| import { transformFile2Base64 } from '@/utils/file-util'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { t } from 'i18next'; | |||
| import { PanelRightClose, Pencil, Upload } from 'lucide-react'; | |||
| import { Pencil, Upload, X } from 'lucide-react'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { LlmModelType, ModelVariableType } from '../dataset/dataset/constant'; | |||
| import { | |||
| LlmModelType, | |||
| ModelVariableType, | |||
| settledModelVariableMap, | |||
| } from '../dataset/dataset/constant'; | |||
| import { | |||
| ISearchAppDetailProps, | |||
| IUpdateSearchProps, | |||
| useUpdateSearch, | |||
| } from '../next-searches/hooks'; | |||
| import { LlmSettingFieldItems } from './search-setting-aisummery-config'; | |||
| import { | |||
| LlmSettingFieldItems, | |||
| LlmSettingSchema, | |||
| } from './search-setting-aisummery-config'; | |||
| interface SearchSettingProps { | |||
| open: boolean; | |||
| @@ -48,6 +55,15 @@ interface SearchSettingProps { | |||
| className?: string; | |||
| data: ISearchAppDetailProps; | |||
| } | |||
| interface ISubmitLlmSettingProps { | |||
| llm_id: string; | |||
| parameter: string; | |||
| temperature?: number; | |||
| top_p?: number; | |||
| frequency_penalty?: number; | |||
| presence_penalty?: number; | |||
| } | |||
| const SearchSettingFormSchema = z | |||
| .object({ | |||
| search_id: z.string().optional(), | |||
| @@ -64,14 +80,7 @@ const SearchSettingFormSchema = z | |||
| use_rerank: z.boolean(), | |||
| top_k: z.number(), | |||
| summary: z.boolean(), | |||
| llm_setting: z.object({ | |||
| llm_id: z.string(), | |||
| parameter: z.string(), | |||
| temperature: z.number(), | |||
| top_p: z.union([z.string(), z.number()]), | |||
| frequency_penalty: z.number(), | |||
| presence_penalty: z.number(), | |||
| }), | |||
| llm_setting: z.object(LlmSettingSchema), | |||
| related_search: z.boolean(), | |||
| query_mindmap: z.boolean(), | |||
| }), | |||
| @@ -133,10 +142,26 @@ const SearchSetting: React.FC<SearchSettingProps> = ({ | |||
| llm_setting: { | |||
| llm_id: llm_setting?.llm_id || '', | |||
| parameter: llm_setting?.parameter || ModelVariableType.Improvise, | |||
| temperature: llm_setting?.temperature || 0.8, | |||
| top_p: llm_setting?.top_p || 0.9, | |||
| frequency_penalty: llm_setting?.frequency_penalty || 0.1, | |||
| presence_penalty: llm_setting?.presence_penalty || 0.1, | |||
| temperature: | |||
| llm_setting?.temperature || | |||
| settledModelVariableMap[ModelVariableType.Improvise].temperature, | |||
| top_p: | |||
| llm_setting?.top_p || | |||
| settledModelVariableMap[ModelVariableType.Improvise].top_p, | |||
| frequency_penalty: | |||
| llm_setting?.frequency_penalty || | |||
| settledModelVariableMap[ModelVariableType.Improvise] | |||
| .frequency_penalty, | |||
| presence_penalty: | |||
| llm_setting?.presence_penalty || | |||
| settledModelVariableMap[ModelVariableType.Improvise] | |||
| .presence_penalty, | |||
| temperatureEnabled: llm_setting?.temperature ? true : false, | |||
| topPEnabled: llm_setting?.top_p ? true : false, | |||
| presencePenaltyEnabled: llm_setting?.presence_penalty ? true : false, | |||
| frequencyPenaltyEnabled: llm_setting?.frequency_penalty | |||
| ? true | |||
| : false, | |||
| }, | |||
| chat_settingcross_languages: [], | |||
| highlight: false, | |||
| @@ -193,7 +218,10 @@ const SearchSetting: React.FC<SearchSettingProps> = ({ | |||
| setDatasetList(datasetListMap); | |||
| }, [datasetListOrigin, datasetSelectEmbdId]); | |||
| const handleDatasetSelectChange = (value, onChange) => { | |||
| const handleDatasetSelectChange = ( | |||
| value: string[], | |||
| onChange: (value: string[]) => void, | |||
| ) => { | |||
| console.log(value); | |||
| if (value.length) { | |||
| const data = datasetListOrigin?.find((item) => item.id === value[0]); | |||
| @@ -224,18 +252,44 @@ const SearchSetting: React.FC<SearchSettingProps> = ({ | |||
| name: 'search_config.summary', | |||
| }); | |||
| const { updateSearch, isLoading: isUpdating } = useUpdateSearch(); | |||
| const { updateSearch } = useUpdateSearch(); | |||
| const { data: systemSetting } = useFetchTenantInfo(); | |||
| const onSubmit = async ( | |||
| formData: IUpdateSearchProps & { tenant_id: string }, | |||
| ) => { | |||
| try { | |||
| const { search_config, ...other_formdata } = formData; | |||
| const { llm_setting, ...other_config } = search_config; | |||
| const llmSetting = { | |||
| llm_id: llm_setting.llm_id, | |||
| parameter: llm_setting.parameter, | |||
| temperature: llm_setting.temperature, | |||
| top_p: llm_setting.top_p, | |||
| frequency_penalty: llm_setting.frequency_penalty, | |||
| presence_penalty: llm_setting.presence_penalty, | |||
| } as ISubmitLlmSettingProps; | |||
| if (!llm_setting.frequencyPenaltyEnabled) { | |||
| delete llmSetting.frequency_penalty; | |||
| } | |||
| if (!llm_setting.presencePenaltyEnabled) { | |||
| delete llmSetting.presence_penalty; | |||
| } | |||
| if (!llm_setting.temperatureEnabled) { | |||
| delete llmSetting.temperature; | |||
| } | |||
| if (!llm_setting.topPEnabled) { | |||
| delete llmSetting.top_p; | |||
| } | |||
| await updateSearch({ | |||
| ...formData, | |||
| ...other_formdata, | |||
| search_config: { | |||
| ...other_config, | |||
| llm_setting: { ...llmSetting }, | |||
| }, | |||
| tenant_id: systemSetting.tenant_id, | |||
| avatar: avatarBase64Str, | |||
| }); | |||
| setOpen(false); // 关闭弹窗 | |||
| setOpen(false); | |||
| } catch (error) { | |||
| console.error('Failed to update search:', error); | |||
| } | |||
| @@ -256,10 +310,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({ | |||
| <div className="flex justify-between items-center text-base mb-8"> | |||
| <div className="text-text-primary">Search Settings</div> | |||
| <div onClick={() => setOpen(false)}> | |||
| <PanelRightClose | |||
| size={16} | |||
| className="text-text-primary cursor-pointer" | |||
| /> | |||
| <X size={16} className="text-text-primary cursor-pointer" /> | |||
| </div> | |||
| </div> | |||
| <div | |||
| @@ -271,7 +322,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({ | |||
| onSubmit={formMethods.handleSubmit( | |||
| (data) => { | |||
| console.log('Form submitted with data:', data); | |||
| onSubmit(data as IUpdateSearchProps); | |||
| onSubmit(data as unknown as IUpdateSearchProps); | |||
| }, | |||
| (errors) => { | |||
| console.log('Validation errors:', errors); | |||
| @@ -462,26 +513,37 @@ const SearchSetting: React.FC<SearchSettingProps> = ({ | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={formMethods.control} | |||
| name="search_config.top_k" | |||
| render={({ field }) => ( | |||
| <FormItem className="flex flex-col"> | |||
| <FormItem> | |||
| <FormLabel>Top K</FormLabel> | |||
| <FormControl> | |||
| <div className="flex justify-between items-center"> | |||
| <div | |||
| className={cn( | |||
| 'flex items-center gap-4 justify-between', | |||
| className, | |||
| )} | |||
| > | |||
| <FormControl> | |||
| <SingleFormSlider | |||
| {...field} | |||
| max={100} | |||
| min={0} | |||
| step={1} | |||
| value={field.value as number} | |||
| onChange={(values) => field.onChange(values)} | |||
| ></SingleFormSlider> | |||
| <Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20"> | |||
| {field.value} | |||
| </Label> | |||
| </div> | |||
| </FormControl> | |||
| </FormControl> | |||
| <FormControl> | |||
| <Input | |||
| type={'number'} | |||
| className="h-7 w-20 bg-bg-card" | |||
| max={100} | |||
| min={0} | |||
| step={1} | |||
| {...field} | |||
| ></Input> | |||
| </FormControl> | |||
| </div> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| @@ -1,22 +1,107 @@ | |||
| import { FileIcon } from '@/components/icon-font'; | |||
| import { ImageWithPopover } from '@/components/image'; | |||
| import { Input } from '@/components/originui/input'; | |||
| import { useClickDrawer } from '@/components/pdf-drawer/hooks'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Popover, | |||
| PopoverContent, | |||
| PopoverTrigger, | |||
| } from '@/components/ui/popover'; | |||
| import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | |||
| import { Skeleton } from '@/components/ui/skeleton'; | |||
| import { Spin } from '@/components/ui/spin'; | |||
| import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; | |||
| import { useGetPaginationWithRouter } from '@/hooks/logic-hooks'; | |||
| import { IReference } from '@/interfaces/database/chat'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Search, X } from 'lucide-react'; | |||
| import { Dispatch, SetStateAction } from 'react'; | |||
| import DOMPurify from 'dompurify'; | |||
| import { t } from 'i18next'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { BrainCircuit, Search, Square, Tag, X } from 'lucide-react'; | |||
| import { | |||
| Dispatch, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useRef, | |||
| } from 'react'; | |||
| import { ISearchAppDetailProps } from '../next-searches/hooks'; | |||
| import { useSendQuestion, useShowMindMapDrawer } from '../search/hooks'; | |||
| import PdfDrawer from './document-preview-modal'; | |||
| import HightLightMarkdown from './highlight-markdown'; | |||
| import './index.less'; | |||
| import styles from './index.less'; | |||
| import MarkdownContent from './markdown-content'; | |||
| import MindMapDrawer from './mindmap-drawer'; | |||
| import RetrievalDocuments from './retrieval-documents'; | |||
| export default function SearchingPage({ | |||
| isSearching, | |||
| searchText, | |||
| data: searchData, | |||
| setIsSearching, | |||
| }: { | |||
| isSearching: boolean; | |||
| searchText: string; | |||
| setIsSearching: Dispatch<SetStateAction<boolean>>; | |||
| setSearchText: Dispatch<SetStateAction<string>>; | |||
| data: ISearchAppDetailProps; | |||
| }) { | |||
| const inputRef = useRef<HTMLInputElement>(null); | |||
| const { | |||
| sendQuestion, | |||
| handleClickRelatedQuestion, | |||
| handleSearchStrChange, | |||
| handleTestChunk, | |||
| setSelectedDocumentIds, | |||
| answer, | |||
| sendingLoading, | |||
| relatedQuestions, | |||
| searchStr, | |||
| loading, | |||
| isFirstRender, | |||
| selectedDocumentIds, | |||
| isSearchStrEmpty, | |||
| setSearchStr, | |||
| stopOutputMessage, | |||
| } = useSendQuestion(searchData.search_config.kb_ids); | |||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | |||
| useClickDrawer(); | |||
| useEffect(() => { | |||
| if (searchText) { | |||
| setSearchStr(searchText); | |||
| sendQuestion(searchText); | |||
| } | |||
| // regain focus | |||
| if (inputRef.current) { | |||
| inputRef.current.focus(); | |||
| } | |||
| }, [searchText, sendQuestion, setSearchStr]); | |||
| const { | |||
| mindMapVisible, | |||
| hideMindMapModal, | |||
| showMindMapModal, | |||
| mindMapLoading, | |||
| mindMap, | |||
| } = useShowMindMapDrawer(searchData.search_config.kb_ids, searchStr); | |||
| const { chunks, total } = useSelectTestingResult(); | |||
| const handleSearch = useCallback(() => { | |||
| sendQuestion(searchStr); | |||
| }, [searchStr, sendQuestion]); | |||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | |||
| const onChange = (pageNumber: number, pageSize: number) => { | |||
| setPagination({ page: pageNumber, pageSize }); | |||
| handleTestChunk(selectedDocumentIds, pageNumber, pageSize); | |||
| }; | |||
| return ( | |||
| <section | |||
| className={cn( | |||
| 'relative w-full flex transition-all justify-start items-center', | |||
| )} | |||
| > | |||
| {/* search header */} | |||
| <div | |||
| className={cn( | |||
| 'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full', | |||
| @@ -24,41 +109,224 @@ export default function SearchingPage({ | |||
| > | |||
| <h1 | |||
| className={cn( | |||
| 'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text', | |||
| 'text-4xl font-bold bg-gradient-to-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text cursor-pointer', | |||
| )} | |||
| onClick={() => { | |||
| setIsSearching(false); | |||
| }} | |||
| > | |||
| RAGFlow | |||
| </h1> | |||
| <div | |||
| className={cn( | |||
| ' rounded-lg text-primary text-xl sticky flex justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ', | |||
| ' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ', | |||
| )} | |||
| > | |||
| <div className={cn('flex flex-col justify-start items-start w-full')}> | |||
| <div className="relative w-full text-primary"> | |||
| <Input | |||
| ref={inputRef} | |||
| key="search-input" | |||
| placeholder="How can I help you today?" | |||
| className={cn( | |||
| 'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background', | |||
| )} | |||
| value={searchStr} | |||
| onChange={handleSearchStrChange} | |||
| disabled={sendingLoading} | |||
| onKeyUp={(e) => { | |||
| if (e.key === 'Enter') { | |||
| handleSearch(); | |||
| } | |||
| }} | |||
| /> | |||
| <div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1"> | |||
| <X />| | |||
| <X | |||
| className="text-text-secondary" | |||
| size={14} | |||
| onClick={() => { | |||
| handleClickRelatedQuestion(''); | |||
| }} | |||
| /> | |||
| <span className="text-text-secondary">|</span> | |||
| <button | |||
| type="button" | |||
| className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4" | |||
| onClick={() => { | |||
| setIsSearching(!isSearching); | |||
| if (sendingLoading) { | |||
| stopOutputMessage(); | |||
| } else { | |||
| handleSearch(); | |||
| } | |||
| }} | |||
| > | |||
| <Search size={22} className="m-auto" /> | |||
| {sendingLoading ? ( | |||
| <Square size={22} className="m-auto" /> | |||
| ) : ( | |||
| <Search size={22} className="m-auto" /> | |||
| )} | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {/* search body */} | |||
| <div | |||
| className="w-full mt-5 overflow-auto scrollbar-none " | |||
| style={{ height: 'calc(100vh - 250px)' }} | |||
| > | |||
| {searchData.search_config.summary && ( | |||
| <> | |||
| <div className="flex justify-start items-start text-text-primary text-2xl"> | |||
| AI Summary | |||
| </div> | |||
| {isEmpty(answer) && sendingLoading ? ( | |||
| <div className="space-y-2"> | |||
| <Skeleton className="h-4 w-full bg-bg-card" /> | |||
| <Skeleton className="h-4 w-full bg-bg-card" /> | |||
| <Skeleton className="h-4 w-2/3 bg-bg-card" /> | |||
| </div> | |||
| ) : ( | |||
| answer.answer && ( | |||
| <div className="border rounded-lg p-4 mt-3 max-h-52 overflow-auto scrollbar-none"> | |||
| <MarkdownContent | |||
| loading={sendingLoading} | |||
| content={answer.answer} | |||
| reference={answer.reference ?? ({} as IReference)} | |||
| clickDocumentButton={clickDocumentButton} | |||
| ></MarkdownContent> | |||
| </div> | |||
| ) | |||
| )} | |||
| </> | |||
| )} | |||
| <div className="w-full border-b border-border-default/80 my-6"></div> | |||
| {/* retrieval documents */} | |||
| <div className=" mt-3 w-44 "> | |||
| <RetrievalDocuments | |||
| selectedDocumentIds={selectedDocumentIds} | |||
| setSelectedDocumentIds={setSelectedDocumentIds} | |||
| onTesting={handleTestChunk} | |||
| ></RetrievalDocuments> | |||
| </div> | |||
| <div className="w-full border-b border-border-default/80 my-6"></div> | |||
| <div className="mt-3 "> | |||
| <Spin spinning={loading}> | |||
| {chunks?.length > 0 && ( | |||
| <> | |||
| {chunks.map((chunk, index) => { | |||
| return ( | |||
| <> | |||
| <div | |||
| key={chunk.chunk_id} | |||
| className="w-full flex flex-col" | |||
| > | |||
| <div className="w-full"> | |||
| <ImageWithPopover | |||
| id={chunk.img_id} | |||
| ></ImageWithPopover> | |||
| <Popover> | |||
| <PopoverTrigger asChild> | |||
| <div | |||
| dangerouslySetInnerHTML={{ | |||
| __html: DOMPurify.sanitize( | |||
| `${chunk.highlight}...`, | |||
| ), | |||
| }} | |||
| className="text-sm text-text-primary mb-1" | |||
| ></div> | |||
| </PopoverTrigger> | |||
| <PopoverContent className="text-text-primary"> | |||
| <HightLightMarkdown> | |||
| {chunk.content_with_weight} | |||
| </HightLightMarkdown> | |||
| </PopoverContent> | |||
| </Popover> | |||
| </div> | |||
| <div | |||
| className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit" | |||
| onClick={() => | |||
| clickDocumentButton(chunk.doc_id, chunk as any) | |||
| } | |||
| > | |||
| <FileIcon name={chunk.docnm_kwd}></FileIcon> | |||
| {chunk.docnm_kwd} | |||
| </div> | |||
| </div> | |||
| {index < chunks.length - 1 && ( | |||
| <div className="w-full border-b border-border-default/80 mt-6"></div> | |||
| )} | |||
| </> | |||
| ); | |||
| })} | |||
| </> | |||
| )} | |||
| </Spin> | |||
| {relatedQuestions?.length > 0 && ( | |||
| <div title={t('chat.relatedQuestion')}> | |||
| <div className="flex gap-2"> | |||
| {relatedQuestions?.map((x, idx) => ( | |||
| <Tag | |||
| key={idx} | |||
| className={styles.tag} | |||
| onClick={handleClickRelatedQuestion(x)} | |||
| > | |||
| {x} | |||
| </Tag> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| <div className="mt-8 px-8 pb-8"> | |||
| <RAGFlowPagination | |||
| current={pagination.current} | |||
| pageSize={pagination.pageSize} | |||
| total={total} | |||
| onChange={onChange} | |||
| ></RAGFlowPagination> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {!mindMapVisible && | |||
| !isFirstRender && | |||
| !isSearchStrEmpty && | |||
| !isEmpty(searchData.search_config.kb_ids) && ( | |||
| <Popover> | |||
| <PopoverTrigger asChild> | |||
| <Button | |||
| className="rounded-lg h-8 w-8 p-0 absolute top-28 right-3 z-30" | |||
| variant={'transparent'} | |||
| onClick={showMindMapModal} | |||
| > | |||
| {/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */} | |||
| <BrainCircuit size={24} /> | |||
| </Button> | |||
| </PopoverTrigger> | |||
| <PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent> | |||
| </Popover> | |||
| )} | |||
| {visible && ( | |||
| <PdfDrawer | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| documentId={documentId} | |||
| chunk={selectedChunk} | |||
| ></PdfDrawer> | |||
| )} | |||
| {mindMapVisible && ( | |||
| <div className="absolute top-20 right-16 z-30"> | |||
| <MindMapDrawer | |||
| visible={mindMapVisible} | |||
| hideModal={hideMindMapModal} | |||
| data={mindMap} | |||
| loading={mindMapLoading} | |||
| ></MindMapDrawer> | |||
| </div> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -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<SpotlightProps> = ({ className }) => { | |||
| const isDark = useIsDarkTheme(); | |||
| console.log('isDark', isDark); | |||
| return ( | |||
| <div | |||
| className={`absolute inset-0 opacity-80 ${className} rounded-lg`} | |||
| @@ -16,8 +19,9 @@ const Spotlight: React.FC<SpotlightProps> = ({ className }) => { | |||
| <div | |||
| className="absolute inset-0" | |||
| style={{ | |||
| background: | |||
| 'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)', | |||
| background: isDark | |||
| ? 'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)' | |||
| : 'radial-gradient(circle at 50% 190%, #E4F3FF 0%, #E4F3FF00 60%)', | |||
| pointerEvents: 'none', | |||
| }} | |||
| ></div> | |||
| @@ -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<CreateSearchResponse, Error, CreateSearchProps>({ | |||
| @@ -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<DeleteSearchResponse, Error, DeleteSearchProps>({ | |||
| @@ -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<ISearchAppDetailProps, 'id'> & { | |||
| export const useUpdateSearch = () => { | |||
| const { t } = useTranslation(); | |||
| const queryClient = useQueryClient(); | |||
| const { | |||
| data, | |||
| isLoading, | |||
| isError, | |||
| mutateAsync: updateSearchMutation, | |||
| } = useMutation<any, Error, IUpdateSearchProps>({ | |||
| @@ -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 }; | |||
| }; | |||
| @@ -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<typeof searchFormSchema>; | |||
| 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 <SearchCard key={x.id} data={x}></SearchCard>; | |||
| })} | |||
| {/* {data.map((x) => { | |||
| return <SearchCard key={x.id} data={x}></SearchCard>; | |||
| })} */} | |||
| </div> | |||
| {list?.data.total && ( | |||
| <RAGFlowPagination | |||
| {...pick(searchParams, 'current', 'pageSize')} | |||
| total={list?.data.total} | |||
| onChange={handlePageChange} | |||
| on | |||
| /> | |||
| )} | |||
| <Modal | |||
| @@ -135,6 +135,7 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| answer: currentAnswer, | |||
| relatedQuestions: relatedQuestions?.slice(0, 5) ?? [], | |||
| searchStr, | |||
| setSearchStr, | |||
| isFirstRender, | |||
| selectedDocumentIds, | |||
| isSearchStrEmpty: isEmpty(trim(searchStr)), | |||