mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### 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)
This commit is contained in:
@ -50,3 +50,4 @@ const Input = function ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { Input };
|
export { Input };
|
||||||
|
export default React.forwardRef(Input);
|
||||||
|
|||||||
@ -152,7 +152,7 @@ const Modal: ModalType = ({
|
|||||||
onClick={() => maskClosable && onOpenChange?.(false)}
|
onClick={() => maskClosable && onOpenChange?.(false)}
|
||||||
>
|
>
|
||||||
<DialogPrimitive.Content
|
<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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* title */}
|
{/* title */}
|
||||||
|
|||||||
@ -42,3 +42,92 @@ export const FormTooltip = ({ tooltip }: { tooltip: React.ReactNode }) => {
|
|||||||
</Tooltip>
|
</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 request from '@/utils/request';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
interface CSVData {
|
interface CSVData {
|
||||||
rows: string[][];
|
rows: string[][];
|
||||||
@ -12,13 +11,14 @@ interface CSVData {
|
|||||||
|
|
||||||
interface FileViewerProps {
|
interface FileViewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CSVFileViewer: React.FC<FileViewerProps> = () => {
|
const CSVFileViewer: React.FC<FileViewerProps> = ({ url }) => {
|
||||||
const [data, setData] = useState<CSVData | null>(null);
|
const [data, setData] = useState<CSVData | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const url = useGetDocumentUrl();
|
// const url = useGetDocumentUrl();
|
||||||
const parseCSV = (csvText: string): CSVData => {
|
const parseCSV = (csvText: string): CSVData => {
|
||||||
console.log('Parsing CSV data:', csvText);
|
console.log('Parsing CSV data:', csvText);
|
||||||
const lines = csvText.split('\n');
|
const lines = csvText.split('\n');
|
||||||
|
|||||||
@ -4,14 +4,17 @@ import request from '@/utils/request';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import mammoth from 'mammoth';
|
import mammoth from 'mammoth';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
interface DocPreviewerProps {
|
interface DocPreviewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocPreviewer: React.FC<DocPreviewerProps> = ({ className }) => {
|
export const DocPreviewer: React.FC<DocPreviewerProps> = ({
|
||||||
const url = useGetDocumentUrl();
|
className,
|
||||||
|
url,
|
||||||
|
}) => {
|
||||||
|
// const url = useGetDocumentUrl();
|
||||||
const [htmlContent, setHtmlContent] = useState<string>('');
|
const [htmlContent, setHtmlContent] = useState<string>('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const fetchDocument = async () => {
|
const fetchDocument = async () => {
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import { useFetchExcel } from '@/pages/document-viewer/hooks';
|
import { useFetchExcel } from '@/pages/document-viewer/hooks';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
interface ExcelCsvPreviewerProps {
|
interface ExcelCsvPreviewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExcelCsvPreviewer: React.FC<ExcelCsvPreviewerProps> = ({
|
export const ExcelCsvPreviewer: React.FC<ExcelCsvPreviewerProps> = ({
|
||||||
className,
|
className,
|
||||||
|
url,
|
||||||
}) => {
|
}) => {
|
||||||
const url = useGetDocumentUrl();
|
// const url = useGetDocumentUrl();
|
||||||
const { containerRef } = useFetchExcel(url);
|
const { containerRef } = useFetchExcel(url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,16 +3,17 @@ import { Spin } from '@/components/ui/spin';
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
interface ImagePreviewerProps {
|
interface ImagePreviewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
|
export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
|
||||||
className,
|
className,
|
||||||
|
url,
|
||||||
}) => {
|
}) => {
|
||||||
const url = useGetDocumentUrl();
|
// const url = useGetDocumentUrl();
|
||||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
|||||||
@ -12,12 +12,14 @@ import { TxtPreviewer } from './txt-preview';
|
|||||||
type PreviewProps = {
|
type PreviewProps = {
|
||||||
fileType: string;
|
fileType: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
url: string;
|
||||||
};
|
};
|
||||||
const Preview = ({
|
const Preview = ({
|
||||||
fileType,
|
fileType,
|
||||||
className,
|
className,
|
||||||
highlights,
|
highlights,
|
||||||
setWidthAndHeight,
|
setWidthAndHeight,
|
||||||
|
url,
|
||||||
}: PreviewProps & Partial<IProps>) => {
|
}: PreviewProps & Partial<IProps>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -26,37 +28,38 @@ const Preview = ({
|
|||||||
<PdfPreviewer
|
<PdfPreviewer
|
||||||
highlights={highlights}
|
highlights={highlights}
|
||||||
setWidthAndHeight={setWidthAndHeight}
|
setWidthAndHeight={setWidthAndHeight}
|
||||||
|
url={url}
|
||||||
></PdfPreviewer>
|
></PdfPreviewer>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{['doc', 'docx'].indexOf(fileType) > -1 && (
|
{['doc', 'docx'].indexOf(fileType) > -1 && (
|
||||||
<section>
|
<section>
|
||||||
<DocPreviewer className={className} />
|
<DocPreviewer className={className} url={url} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{['txt', 'md'].indexOf(fileType) > -1 && (
|
{['txt', 'md'].indexOf(fileType) > -1 && (
|
||||||
<section>
|
<section>
|
||||||
<TxtPreviewer className={className} />
|
<TxtPreviewer className={className} url={url} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{['visual'].indexOf(fileType) > -1 && (
|
{['visual'].indexOf(fileType) > -1 && (
|
||||||
<section>
|
<section>
|
||||||
<ImagePreviewer className={className} />
|
<ImagePreviewer className={className} url={url} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{['pptx'].indexOf(fileType) > -1 && (
|
{['pptx'].indexOf(fileType) > -1 && (
|
||||||
<section>
|
<section>
|
||||||
<PptPreviewer className={className} />
|
<PptPreviewer className={className} url={url} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{['xlsx'].indexOf(fileType) > -1 && (
|
{['xlsx'].indexOf(fileType) > -1 && (
|
||||||
<section>
|
<section>
|
||||||
<ExcelCsvPreviewer className={className} />
|
<ExcelCsvPreviewer className={className} url={url} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{['csv'].indexOf(fileType) > -1 && (
|
{['csv'].indexOf(fileType) > -1 && (
|
||||||
<section>
|
<section>
|
||||||
<CSVFileViewer className={className} />
|
<CSVFileViewer className={className} url={url} />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
PdfLoader,
|
PdfLoader,
|
||||||
Popup,
|
Popup,
|
||||||
} from 'react-pdf-highlighter';
|
} from 'react-pdf-highlighter';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
|
import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
|
||||||
import { Spin } from '@/components/ui/spin';
|
import { Spin } from '@/components/ui/spin';
|
||||||
@ -17,6 +16,7 @@ import styles from './index.less';
|
|||||||
export interface IProps {
|
export interface IProps {
|
||||||
highlights: IHighlight[];
|
highlights: IHighlight[];
|
||||||
setWidthAndHeight: (width: number, height: number) => void;
|
setWidthAndHeight: (width: number, height: number) => void;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
const HighlightPopup = ({
|
const HighlightPopup = ({
|
||||||
comment,
|
comment,
|
||||||
@ -30,8 +30,8 @@ const HighlightPopup = ({
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
// TODO: merge with DocumentPreviewer
|
// TODO: merge with DocumentPreviewer
|
||||||
const PdfPreview = ({ highlights: state, setWidthAndHeight }: IProps) => {
|
const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
|
||||||
const url = useGetDocumentUrl();
|
// const url = useGetDocumentUrl();
|
||||||
|
|
||||||
const ref = useRef<(highlight: IHighlight) => void>(() => {});
|
const ref = useRef<(highlight: IHighlight) => void>(() => {});
|
||||||
const error = useCatchDocumentError(url);
|
const error = useCatchDocumentError(url);
|
||||||
|
|||||||
@ -3,13 +3,16 @@ import request from '@/utils/request';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { init } from 'pptx-preview';
|
import { init } from 'pptx-preview';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
interface PptPreviewerProps {
|
interface PptPreviewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PptPreviewer: React.FC<PptPreviewerProps> = ({ className }) => {
|
export const PptPreviewer: React.FC<PptPreviewerProps> = ({
|
||||||
const url = useGetDocumentUrl();
|
className,
|
||||||
|
url,
|
||||||
|
}) => {
|
||||||
|
// const url = useGetDocumentUrl();
|
||||||
const wrapper = useRef<HTMLDivElement>(null);
|
const wrapper = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const fetchDocument = async () => {
|
const fetchDocument = async () => {
|
||||||
|
|||||||
@ -3,11 +3,10 @@ import { Spin } from '@/components/ui/spin';
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useGetDocumentUrl } from './hooks';
|
|
||||||
|
|
||||||
type TxtPreviewerProps = { className?: string };
|
type TxtPreviewerProps = { className?: string; url: string };
|
||||||
export const TxtPreviewer = ({ className }: TxtPreviewerProps) => {
|
export const TxtPreviewer = ({ className, url }: TxtPreviewerProps) => {
|
||||||
const url = useGetDocumentUrl();
|
// const url = useGetDocumentUrl();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<string>('');
|
const [data, setData] = useState<string>('');
|
||||||
const fetchTxt = async () => {
|
const fetchTxt = async () => {
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import {
|
|||||||
useNavigatePage,
|
useNavigatePage,
|
||||||
} from '@/hooks/logic-hooks/navigate-hooks';
|
} from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||||
|
import { useGetDocumentUrl } from '../../../knowledge-chunk/components/document-preview/hooks';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
const Chunk = () => {
|
const Chunk = () => {
|
||||||
@ -73,6 +74,7 @@ const Chunk = () => {
|
|||||||
} = useUpdateChunk();
|
} = useUpdateChunk();
|
||||||
const { navigateToDataset, getQueryString, navigateToDatasetList } =
|
const { navigateToDataset, getQueryString, navigateToDatasetList } =
|
||||||
useNavigatePage();
|
useNavigatePage();
|
||||||
|
const fileUrl = useGetDocumentUrl();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChunkList(data);
|
setChunkList(data);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@ -212,6 +214,7 @@ const Chunk = () => {
|
|||||||
fileType={fileType}
|
fileType={fileType}
|
||||||
highlights={highlights}
|
highlights={highlights}
|
||||||
setWidthAndHeight={setWidthAndHeight}
|
setWidthAndHeight={setWidthAndHeight}
|
||||||
|
url={fileUrl}
|
||||||
></DocumentPreview>
|
></DocumentPreview>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
29
web/src/pages/next-search/document-preview-modal/hooks.ts
Normal file
29
web/src/pages/next-search/document-preview-modal/hooks.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
65
web/src/pages/next-search/document-preview-modal/index.tsx
Normal file
65
web/src/pages/next-search/document-preview-modal/index.tsx
Normal file
@ -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;
|
||||||
48
web/src/pages/next-search/highlight-markdown/index.tsx
Normal file
48
web/src/pages/next-search/highlight-markdown/index.tsx
Normal file
@ -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 {
|
.delay-700 {
|
||||||
animation-delay: 0.7s;
|
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 { Button } from '@/components/ui/button';
|
||||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
import { Settings } from 'lucide-react';
|
import { Settings } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ISearchAppDetailProps,
|
ISearchAppDetailProps,
|
||||||
useFetchSearchDetail,
|
useFetchSearchDetail,
|
||||||
@ -26,6 +26,13 @@ export default function SearchPage() {
|
|||||||
const { data: SearchData } = useFetchSearchDetail();
|
const { data: SearchData } = useFetchSearchDetail();
|
||||||
|
|
||||||
const [openSetting, setOpenSetting] = useState(false);
|
const [openSetting, setOpenSetting] = useState(false);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSearching) {
|
||||||
|
setOpenSetting(false);
|
||||||
|
}
|
||||||
|
}, [isSearching]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@ -50,6 +57,8 @@ export default function SearchPage() {
|
|||||||
<SearchHome
|
<SearchHome
|
||||||
setIsSearching={setIsSearching}
|
setIsSearching={setIsSearching}
|
||||||
isSearching={isSearching}
|
isSearching={isSearching}
|
||||||
|
searchText={searchText}
|
||||||
|
setSearchText={setSearchText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -57,23 +66,24 @@ export default function SearchPage() {
|
|||||||
<div className="animate-fade-in-up">
|
<div className="animate-fade-in-up">
|
||||||
<SearchingPage
|
<SearchingPage
|
||||||
setIsSearching={setIsSearching}
|
setIsSearching={setIsSearching}
|
||||||
isSearching={isSearching}
|
searchText={searchText}
|
||||||
|
setSearchText={setSearchText}
|
||||||
|
data={SearchData as ISearchAppDetailProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* {openSetting && (
|
{openSetting && (
|
||||||
<div className=" w-[440px]"> */}
|
|
||||||
<SearchSetting
|
<SearchSetting
|
||||||
className="mt-20 mr-2"
|
className="mt-20 mr-2"
|
||||||
open={openSetting}
|
open={openSetting}
|
||||||
setOpen={setOpenSetting}
|
setOpen={setOpenSetting}
|
||||||
data={SearchData as ISearchAppDetailProps}
|
data={SearchData as ISearchAppDetailProps}
|
||||||
/>
|
/>
|
||||||
{/* </div>
|
)}
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isSearching && (
|
||||||
<div className="absolute left-5 bottom-12 ">
|
<div className="absolute left-5 bottom-12 ">
|
||||||
<Button
|
<Button
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
@ -84,6 +94,7 @@ export default function SearchPage() {
|
|||||||
<div className="text-text-secondary">Search Settings</div>
|
<div className="text-text-secondary">Search Settings</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
295
web/src/pages/next-search/markdown-content/index.tsx
Normal file
295
web/src/pages/next-search/markdown-content/index.tsx
Normal file
@ -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;
|
||||||
47
web/src/pages/next-search/mindmap-drawer.tsx
Normal file
47
web/src/pages/next-search/mindmap-drawer.tsx
Normal file
@ -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;
|
||||||
11
web/src/pages/next-search/retrieval-documents/index.less
Normal file
11
web/src/pages/next-search/retrieval-documents/index.less
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.selectFilesCollapse {
|
||||||
|
:global(.ant-collapse-header) {
|
||||||
|
padding-left: 22px;
|
||||||
|
}
|
||||||
|
margin-bottom: 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectFilesTitle {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
237
web/src/pages/next-search/retrieval-documents/index.tsx
Normal file
237
web/src/pages/next-search/retrieval-documents/index.tsx
Normal file
@ -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 { Input } from '@/components/originui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
@ -9,10 +10,15 @@ import Spotlight from './spotlight';
|
|||||||
export default function SearchPage({
|
export default function SearchPage({
|
||||||
isSearching,
|
isSearching,
|
||||||
setIsSearching,
|
setIsSearching,
|
||||||
|
searchText,
|
||||||
|
setSearchText,
|
||||||
}: {
|
}: {
|
||||||
isSearching: boolean;
|
isSearching: boolean;
|
||||||
setIsSearching: Dispatch<SetStateAction<boolean>>;
|
setIsSearching: Dispatch<SetStateAction<boolean>>;
|
||||||
|
searchText: string;
|
||||||
|
setSearchText: Dispatch<SetStateAction<string>>;
|
||||||
}) {
|
}) {
|
||||||
|
const { data: userInfo } = useFetchUserInfo();
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full flex transition-all justify-center items-center mt-32">
|
<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]">
|
<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 && (
|
{!isSearching && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-4 transition-opacity">👋 Hi there</p>
|
<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 ">
|
<div className="relative w-full ">
|
||||||
<Input
|
<Input
|
||||||
placeholder="How can I help you today?"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -33,15 +33,16 @@ interface LlmSettingFieldItemsProps {
|
|||||||
|
|
||||||
export const LlmSettingSchema = {
|
export const LlmSettingSchema = {
|
||||||
llm_id: z.string(),
|
llm_id: z.string(),
|
||||||
|
parameter: z.string(),
|
||||||
temperature: z.coerce.number(),
|
temperature: z.coerce.number(),
|
||||||
top_p: z.string(),
|
top_p: z.coerce.number(),
|
||||||
presence_penalty: z.coerce.number(),
|
presence_penalty: z.coerce.number(),
|
||||||
frequency_penalty: z.coerce.number(),
|
frequency_penalty: z.coerce.number(),
|
||||||
temperatureEnabled: z.boolean(),
|
temperatureEnabled: z.boolean(),
|
||||||
topPEnabled: z.boolean(),
|
topPEnabled: z.boolean(),
|
||||||
presencePenaltyEnabled: z.boolean(),
|
presencePenaltyEnabled: z.boolean(),
|
||||||
frequencyPenaltyEnabled: z.boolean(),
|
frequencyPenaltyEnabled: z.boolean(),
|
||||||
maxTokensEnabled: z.boolean(),
|
// maxTokensEnabled: z.boolean(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LlmSettingFieldItems({
|
export function LlmSettingFieldItems({
|
||||||
@ -58,7 +59,8 @@ export function LlmSettingFieldItems({
|
|||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(parameter: string) => {
|
(parameter: string) => {
|
||||||
// const currentValues = { ...form.getValues() };
|
const currentValues = { ...form.getValues() };
|
||||||
|
console.log('currentValues', currentValues);
|
||||||
const values =
|
const values =
|
||||||
settledModelVariableMap[
|
settledModelVariableMap[
|
||||||
parameter as keyof typeof settledModelVariableMap
|
parameter as keyof typeof settledModelVariableMap
|
||||||
@ -145,28 +147,28 @@ export function LlmSettingFieldItems({
|
|||||||
/>
|
/>
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('temperature')}
|
name={getFieldWithPrefix('temperature')}
|
||||||
checkName="temperatureEnabled"
|
checkName={getFieldWithPrefix('temperatureEnabled')}
|
||||||
label="temperature"
|
label="temperature"
|
||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('top_p')}
|
name={getFieldWithPrefix('top_p')}
|
||||||
checkName="topPEnabled"
|
checkName={getFieldWithPrefix('topPEnabled')}
|
||||||
label="topP"
|
label="topP"
|
||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('presence_penalty')}
|
name={getFieldWithPrefix('presence_penalty')}
|
||||||
checkName="presencePenaltyEnabled"
|
checkName={getFieldWithPrefix('presencePenaltyEnabled')}
|
||||||
label="presencePenalty"
|
label="presencePenalty"
|
||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('frequency_penalty')}
|
name={getFieldWithPrefix('frequency_penalty')}
|
||||||
checkName="frequencyPenaltyEnabled"
|
checkName={getFieldWithPrefix('frequencyPenaltyEnabled')}
|
||||||
label="frequencyPenalty"
|
label="frequencyPenalty"
|
||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
|
|||||||
@ -30,17 +30,24 @@ import { cn } from '@/lib/utils';
|
|||||||
import { transformFile2Base64 } from '@/utils/file-util';
|
import { transformFile2Base64 } from '@/utils/file-util';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { t } from 'i18next';
|
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 { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { LlmModelType, ModelVariableType } from '../dataset/dataset/constant';
|
import {
|
||||||
|
LlmModelType,
|
||||||
|
ModelVariableType,
|
||||||
|
settledModelVariableMap,
|
||||||
|
} from '../dataset/dataset/constant';
|
||||||
import {
|
import {
|
||||||
ISearchAppDetailProps,
|
ISearchAppDetailProps,
|
||||||
IUpdateSearchProps,
|
IUpdateSearchProps,
|
||||||
useUpdateSearch,
|
useUpdateSearch,
|
||||||
} from '../next-searches/hooks';
|
} from '../next-searches/hooks';
|
||||||
import { LlmSettingFieldItems } from './search-setting-aisummery-config';
|
import {
|
||||||
|
LlmSettingFieldItems,
|
||||||
|
LlmSettingSchema,
|
||||||
|
} from './search-setting-aisummery-config';
|
||||||
|
|
||||||
interface SearchSettingProps {
|
interface SearchSettingProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -48,6 +55,15 @@ interface SearchSettingProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
data: ISearchAppDetailProps;
|
data: ISearchAppDetailProps;
|
||||||
}
|
}
|
||||||
|
interface ISubmitLlmSettingProps {
|
||||||
|
llm_id: string;
|
||||||
|
parameter: string;
|
||||||
|
temperature?: number;
|
||||||
|
top_p?: number;
|
||||||
|
frequency_penalty?: number;
|
||||||
|
presence_penalty?: number;
|
||||||
|
}
|
||||||
|
|
||||||
const SearchSettingFormSchema = z
|
const SearchSettingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
search_id: z.string().optional(),
|
search_id: z.string().optional(),
|
||||||
@ -64,14 +80,7 @@ const SearchSettingFormSchema = z
|
|||||||
use_rerank: z.boolean(),
|
use_rerank: z.boolean(),
|
||||||
top_k: z.number(),
|
top_k: z.number(),
|
||||||
summary: z.boolean(),
|
summary: z.boolean(),
|
||||||
llm_setting: z.object({
|
llm_setting: z.object(LlmSettingSchema),
|
||||||
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(),
|
|
||||||
}),
|
|
||||||
related_search: z.boolean(),
|
related_search: z.boolean(),
|
||||||
query_mindmap: z.boolean(),
|
query_mindmap: z.boolean(),
|
||||||
}),
|
}),
|
||||||
@ -133,10 +142,26 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
llm_setting: {
|
llm_setting: {
|
||||||
llm_id: llm_setting?.llm_id || '',
|
llm_id: llm_setting?.llm_id || '',
|
||||||
parameter: llm_setting?.parameter || ModelVariableType.Improvise,
|
parameter: llm_setting?.parameter || ModelVariableType.Improvise,
|
||||||
temperature: llm_setting?.temperature || 0.8,
|
temperature:
|
||||||
top_p: llm_setting?.top_p || 0.9,
|
llm_setting?.temperature ||
|
||||||
frequency_penalty: llm_setting?.frequency_penalty || 0.1,
|
settledModelVariableMap[ModelVariableType.Improvise].temperature,
|
||||||
presence_penalty: llm_setting?.presence_penalty || 0.1,
|
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: [],
|
chat_settingcross_languages: [],
|
||||||
highlight: false,
|
highlight: false,
|
||||||
@ -193,7 +218,10 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
setDatasetList(datasetListMap);
|
setDatasetList(datasetListMap);
|
||||||
}, [datasetListOrigin, datasetSelectEmbdId]);
|
}, [datasetListOrigin, datasetSelectEmbdId]);
|
||||||
|
|
||||||
const handleDatasetSelectChange = (value, onChange) => {
|
const handleDatasetSelectChange = (
|
||||||
|
value: string[],
|
||||||
|
onChange: (value: string[]) => void,
|
||||||
|
) => {
|
||||||
console.log(value);
|
console.log(value);
|
||||||
if (value.length) {
|
if (value.length) {
|
||||||
const data = datasetListOrigin?.find((item) => item.id === value[0]);
|
const data = datasetListOrigin?.find((item) => item.id === value[0]);
|
||||||
@ -224,18 +252,44 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
name: 'search_config.summary',
|
name: 'search_config.summary',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { updateSearch, isLoading: isUpdating } = useUpdateSearch();
|
const { updateSearch } = useUpdateSearch();
|
||||||
const { data: systemSetting } = useFetchTenantInfo();
|
const { data: systemSetting } = useFetchTenantInfo();
|
||||||
const onSubmit = async (
|
const onSubmit = async (
|
||||||
formData: IUpdateSearchProps & { tenant_id: string },
|
formData: IUpdateSearchProps & { tenant_id: string },
|
||||||
) => {
|
) => {
|
||||||
try {
|
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({
|
await updateSearch({
|
||||||
...formData,
|
...other_formdata,
|
||||||
|
search_config: {
|
||||||
|
...other_config,
|
||||||
|
llm_setting: { ...llmSetting },
|
||||||
|
},
|
||||||
tenant_id: systemSetting.tenant_id,
|
tenant_id: systemSetting.tenant_id,
|
||||||
avatar: avatarBase64Str,
|
avatar: avatarBase64Str,
|
||||||
});
|
});
|
||||||
setOpen(false); // 关闭弹窗
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update search:', 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="flex justify-between items-center text-base mb-8">
|
||||||
<div className="text-text-primary">Search Settings</div>
|
<div className="text-text-primary">Search Settings</div>
|
||||||
<div onClick={() => setOpen(false)}>
|
<div onClick={() => setOpen(false)}>
|
||||||
<PanelRightClose
|
<X size={16} className="text-text-primary cursor-pointer" />
|
||||||
size={16}
|
|
||||||
className="text-text-primary cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -271,7 +322,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
onSubmit={formMethods.handleSubmit(
|
onSubmit={formMethods.handleSubmit(
|
||||||
(data) => {
|
(data) => {
|
||||||
console.log('Form submitted with data:', data);
|
console.log('Form submitted with data:', data);
|
||||||
onSubmit(data as IUpdateSearchProps);
|
onSubmit(data as unknown as IUpdateSearchProps);
|
||||||
},
|
},
|
||||||
(errors) => {
|
(errors) => {
|
||||||
console.log('Validation errors:', errors);
|
console.log('Validation errors:', errors);
|
||||||
@ -462,26 +513,37 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
name="search_config.top_k"
|
name="search_config.top_k"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem>
|
||||||
<FormLabel>Top K</FormLabel>
|
<FormLabel>Top K</FormLabel>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-4 justify-between',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<SingleFormSlider
|
<SingleFormSlider
|
||||||
|
{...field}
|
||||||
max={100}
|
max={100}
|
||||||
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
value={field.value as number}
|
|
||||||
onChange={(values) => field.onChange(values)}
|
|
||||||
></SingleFormSlider>
|
></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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,22 +1,107 @@
|
|||||||
|
import { FileIcon } from '@/components/icon-font';
|
||||||
|
import { ImageWithPopover } from '@/components/image';
|
||||||
import { Input } from '@/components/originui/input';
|
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 { cn } from '@/lib/utils';
|
||||||
import { Search, X } from 'lucide-react';
|
import DOMPurify from 'dompurify';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
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 './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({
|
export default function SearchingPage({
|
||||||
isSearching,
|
searchText,
|
||||||
|
data: searchData,
|
||||||
setIsSearching,
|
setIsSearching,
|
||||||
}: {
|
}: {
|
||||||
isSearching: boolean;
|
searchText: string;
|
||||||
setIsSearching: Dispatch<SetStateAction<boolean>>;
|
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 (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative w-full flex transition-all justify-start items-center',
|
'relative w-full flex transition-all justify-start items-center',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* search header */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10 px-8 pt-8 flex text-transparent justify-start items-start w-full',
|
'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
|
<h1
|
||||||
className={cn(
|
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
|
RAGFlow
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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={cn('flex flex-col justify-start items-start w-full')}>
|
||||||
<div className="relative w-full text-primary">
|
<div className="relative w-full text-primary">
|
||||||
<Input
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
key="search-input"
|
||||||
placeholder="How can I help you today?"
|
placeholder="How can I help you today?"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background',
|
'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">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
|
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsSearching(!isSearching);
|
if (sendingLoading) {
|
||||||
|
stopOutputMessage();
|
||||||
|
} else {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{sendingLoading ? (
|
||||||
|
<Square size={22} className="m-auto" />
|
||||||
|
) : (
|
||||||
<Search size={22} className="m-auto" />
|
<Search size={22} className="m-auto" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface SpotlightProps {
|
interface SpotlightProps {
|
||||||
@ -5,6 +6,8 @@ interface SpotlightProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
|
const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
|
||||||
|
const isDark = useIsDarkTheme();
|
||||||
|
console.log('isDark', isDark);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
|
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
|
||||||
@ -16,8 +19,9 @@ const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background: isDark
|
||||||
'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)',
|
? 'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)'
|
||||||
|
: 'radial-gradient(circle at 50% 190%, #E4F3FF 0%, #E4F3FF00 60%)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// src/pages/next-searches/hooks.ts
|
// src/pages/next-searches/hooks.ts
|
||||||
|
|
||||||
|
import message from '@/components/ui/message';
|
||||||
import searchService from '@/services/search-service';
|
import searchService from '@/services/search-service';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { message } from 'antd';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'umi';
|
import { useParams } from 'umi';
|
||||||
@ -23,7 +23,6 @@ export const useCreateSearch = () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
|
||||||
isError,
|
isError,
|
||||||
mutateAsync: createSearchMutation,
|
mutateAsync: createSearchMutation,
|
||||||
} = useMutation<CreateSearchResponse, Error, CreateSearchProps>({
|
} = useMutation<CreateSearchResponse, Error, CreateSearchProps>({
|
||||||
@ -50,7 +49,7 @@ export const useCreateSearch = () => {
|
|||||||
[createSearchMutation],
|
[createSearchMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, isLoading, isError, createSearch };
|
return { data, isError, createSearch };
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SearchListParams {
|
export interface SearchListParams {
|
||||||
@ -128,7 +127,6 @@ export const useDeleteSearch = () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
|
||||||
isError,
|
isError,
|
||||||
mutateAsync: deleteSearchMutation,
|
mutateAsync: deleteSearchMutation,
|
||||||
} = useMutation<DeleteSearchResponse, Error, DeleteSearchProps>({
|
} = useMutation<DeleteSearchResponse, Error, DeleteSearchProps>({
|
||||||
@ -155,7 +153,7 @@ export const useDeleteSearch = () => {
|
|||||||
[deleteSearchMutation],
|
[deleteSearchMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, isLoading, isError, deleteSearch };
|
return { data, isError, deleteSearch };
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IllmSettingProps {
|
interface IllmSettingProps {
|
||||||
@ -166,7 +164,12 @@ interface IllmSettingProps {
|
|||||||
frequency_penalty: number;
|
frequency_penalty: number;
|
||||||
presence_penalty: number;
|
presence_penalty: number;
|
||||||
}
|
}
|
||||||
|
interface IllmSettingEnableProps {
|
||||||
|
temperatureEnabled?: boolean;
|
||||||
|
topPEnabled?: boolean;
|
||||||
|
presencePenaltyEnabled?: boolean;
|
||||||
|
frequencyPenaltyEnabled?: boolean;
|
||||||
|
}
|
||||||
export interface ISearchAppDetailProps {
|
export interface ISearchAppDetailProps {
|
||||||
avatar: any;
|
avatar: any;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
@ -184,7 +187,7 @@ export interface ISearchAppDetailProps {
|
|||||||
rerank_id: string;
|
rerank_id: string;
|
||||||
similarity_threshold: number;
|
similarity_threshold: number;
|
||||||
summary: boolean;
|
summary: boolean;
|
||||||
llm_setting: IllmSettingProps;
|
llm_setting: IllmSettingProps & IllmSettingEnableProps;
|
||||||
top_k: number;
|
top_k: number;
|
||||||
use_kg: boolean;
|
use_kg: boolean;
|
||||||
vector_similarity_weight: number;
|
vector_similarity_weight: number;
|
||||||
@ -225,10 +228,9 @@ export type IUpdateSearchProps = Omit<ISearchAppDetailProps, 'id'> & {
|
|||||||
|
|
||||||
export const useUpdateSearch = () => {
|
export const useUpdateSearch = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
|
||||||
isError,
|
isError,
|
||||||
mutateAsync: updateSearchMutation,
|
mutateAsync: updateSearchMutation,
|
||||||
} = useMutation<any, Error, IUpdateSearchProps>({
|
} = useMutation<any, Error, IUpdateSearchProps>({
|
||||||
@ -241,8 +243,11 @@ export const useUpdateSearch = () => {
|
|||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (data, variables) => {
|
||||||
message.success(t('message.updated'));
|
message.success(t('message.updated'));
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['searchDetail', variables.search_id],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
message.error(t('message.error', { error: error.message }));
|
message.error(t('message.error', { error: error.message }));
|
||||||
@ -256,5 +261,5 @@ export const useUpdateSearch = () => {
|
|||||||
[updateSearchMutation],
|
[updateSearchMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, isLoading, isError, updateSearch };
|
return { data, isError, updateSearch };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { Modal } from '@/components/ui/modal/modal';
|
import { Modal } from '@/components/ui/modal/modal';
|
||||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
|
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { Plus, Search } from 'lucide-react';
|
import { Plus, Search } from 'lucide-react';
|
||||||
@ -30,6 +31,7 @@ type SearchFormValues = z.infer<typeof searchFormSchema>;
|
|||||||
export default function SearchList() {
|
export default function SearchList() {
|
||||||
// const { data } = useFetchFlowList();
|
// const { data } = useFetchFlowList();
|
||||||
const { t } = useTranslate('search');
|
const { t } = useTranslate('search');
|
||||||
|
const { navigateToSearch } = useNavigatePage();
|
||||||
const { isLoading, createSearch } = useCreateSearch();
|
const { isLoading, createSearch } = useCreateSearch();
|
||||||
const {
|
const {
|
||||||
data: list,
|
data: list,
|
||||||
@ -48,7 +50,10 @@ export default function SearchList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (values: SearchFormValues) => {
|
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) {
|
if (!isLoading) {
|
||||||
setOpenCreateModal(false);
|
setOpenCreateModal(false);
|
||||||
}
|
}
|
||||||
@ -88,16 +93,12 @@ export default function SearchList() {
|
|||||||
{list?.data.search_apps.map((x) => {
|
{list?.data.search_apps.map((x) => {
|
||||||
return <SearchCard key={x.id} data={x}></SearchCard>;
|
return <SearchCard key={x.id} data={x}></SearchCard>;
|
||||||
})}
|
})}
|
||||||
{/* {data.map((x) => {
|
|
||||||
return <SearchCard key={x.id} data={x}></SearchCard>;
|
|
||||||
})} */}
|
|
||||||
</div>
|
</div>
|
||||||
{list?.data.total && (
|
{list?.data.total && (
|
||||||
<RAGFlowPagination
|
<RAGFlowPagination
|
||||||
{...pick(searchParams, 'current', 'pageSize')}
|
{...pick(searchParams, 'current', 'pageSize')}
|
||||||
total={list?.data.total}
|
total={list?.data.total}
|
||||||
onChange={handlePageChange}
|
onChange={handlePageChange}
|
||||||
on
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -135,6 +135,7 @@ export const useSendQuestion = (kbIds: string[]) => {
|
|||||||
answer: currentAnswer,
|
answer: currentAnswer,
|
||||||
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
|
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
|
||||||
searchStr,
|
searchStr,
|
||||||
|
setSearchStr,
|
||||||
isFirstRender,
|
isFirstRender,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
isSearchStrEmpty: isEmpty(trim(searchStr)),
|
isSearchStrEmpty: isEmpty(trim(searchStr)),
|
||||||
|
|||||||
Reference in New Issue
Block a user