feat: add ragflow web project & add pnpm workspace file

This commit is contained in:
2025-11-09 11:18:58 +08:00
parent ed6e0ab282
commit b2053760be
1566 changed files with 218623 additions and 57 deletions

View 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,
};
};

View File

@@ -0,0 +1,67 @@
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 & { docnm_kwd: string; document_name: string };
}
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();
const [fileType, setFileType] = useState('');
useEffect(() => {
if (chunk.docnm_kwd || chunk.document_name) {
const type = getFileExtensionRegex(
chunk.docnm_kwd || chunk.document_name,
);
setFileType(type);
}
}, [chunk.docnm_kwd, chunk.document_name]);
return (
<Modal
title={
<div className="flex items-center gap-2">
<FileIcon name={chunk.docnm_kwd || chunk.document_name}></FileIcon>
{chunk.docnm_kwd || chunk.document_name}
</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;

View File

@@ -0,0 +1,149 @@
import HightLightMarkdown from '@/components/highlight-markdown';
import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
LanguageAbbreviation,
LanguageAbbreviationMap,
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { message } from 'antd';
import { useCallback, useMemo, useState } from 'react';
type IEmbedAppModalProps = {
open: any;
url: string;
token: string;
from: string;
setOpen: (e: any) => void;
tenantId: string;
beta?: string;
};
const EmbedAppModal = (props: IEmbedAppModalProps) => {
const { t } = useTranslate('search');
const { open, setOpen, token = '', from, url, tenantId, beta = '' } = props;
const [hideAvatar, setHideAvatar] = useState(false);
const [locale, setLocale] = useState('');
const languageOptions = useMemo(() => {
return Object.values(LanguageAbbreviation).map((x) => ({
label: LanguageAbbreviationMap[x],
value: x,
}));
}, []);
const generateIframeSrc = useCallback(() => {
// const { visibleAvatar, locale } = values;
let src = `${location.origin}${url}?shared_id=${token}&from=${from}&auth=${beta}&tenantId=${tenantId}`;
if (hideAvatar) {
src += '&visible_avatar=1';
}
if (locale) {
src += `&locale=${locale}`;
}
return src;
}, [beta, from, token, hideAvatar, locale, url, tenantId]);
// ... existing code ...
const text = useMemo(() => {
const iframeSrc = generateIframeSrc();
return `\`\`\`html
<iframe
src="${iframeSrc}"
style="width: 100%; height: 100%; min-height: 600px"
frameborder="0">
</iframe>
\`\`\``;
}, [generateIframeSrc]);
// ... existing code ...
return (
<Modal
title={t('embedIntoSite', { keyPrefix: 'common' })}
className="!bg-bg-base !text-text-disabled"
open={open}
onCancel={() => setOpen(false)}
showfooter={false}
footer={null}
>
<div className="w-full">
{/* Hide Avatar Toggle */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">
{t('profile')}
</label>
<div className="flex items-center">
<Switch
checked={hideAvatar}
onCheckedChange={(value) => {
setHideAvatar(value);
}}
/>
</div>
</div>
{/* Locale Select */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">
{t('locale')}
</label>
<RAGFlowSelect
placeholder="Select a locale"
value={locale}
onChange={(value) => setLocale(value)}
options={languageOptions}
></RAGFlowSelect>
</div>
{/* Embed Code */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">
{t('embedCode')}
</label>
{/* <div className=" border rounded-lg"> */}
{/* <pre className="text-sm whitespace-pre-wrap">{text}</pre> */}
<HightLightMarkdown>{text}</HightLightMarkdown>
{/* </div> */}
</div>
{/* ID Field */}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">{t('id')}</label>
<div className="flex items-center border border-border rounded-lg bg-bg-base">
<input
type="text"
value={token}
readOnly
className="flex-1 px-4 py-2 focus:outline-none bg-bg-base rounded-lg"
/>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(token);
message.success(t('copySuccess'));
}}
className="ml-2 p-2 hover:text-white transition-colors"
title="Copy ID"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h10a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
</div>
</div>
</Modal>
);
};
export default EmbedAppModal;

View 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;

View File

@@ -0,0 +1,541 @@
import message from '@/components/ui/message';
import { SharedFrom } from '@/constants/chat';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import {
useGetPaginationWithRouter,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import { useSetPaginationParams } from '@/hooks/route-hook';
import { useKnowledgeBaseId } from '@/hooks/use-knowledge-request';
import { ResponsePostType } from '@/interfaces/database/base';
import { IAnswer } from '@/interfaces/database/chat';
import { ITestingResult } from '@/interfaces/database/knowledge';
import { IAskRequestBody } from '@/interfaces/request/chat';
import chatService from '@/services/chat-service';
import kbService from '@/services/knowledge-service';
import searchService from '@/services/search-service';
import api from '@/utils/api';
import { useMutation } from '@tanstack/react-query';
import { has, isEmpty, trim } from 'lodash';
import {
ChangeEventHandler,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { useSearchParams } from 'umi';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import { useShowMindMapDrawer } from '../search/hooks';
import { useClickDrawer } from './document-preview-modal/hooks';
export interface ISearchingProps {
searchText?: string;
data: ISearchAppDetailProps;
setIsSearching?: Dispatch<SetStateAction<boolean>>;
setSearchText?: Dispatch<SetStateAction<string>>;
}
export type ISearchReturnProps = ReturnType<typeof useSearching>;
export const useGetSharedSearchParams = () => {
const [searchParams] = useSearchParams();
const data_prefix = 'data_';
const data = Object.fromEntries(
searchParams
.entries()
.filter(([key]) => key.startsWith(data_prefix))
.map(([key, value]) => [key.replace(data_prefix, ''), value]),
);
return {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
tenantId: searchParams.get('tenantId'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
: true,
};
};
export const useSearchFetchMindMap = () => {
const [searchParams] = useSearchParams();
const sharedId = searchParams.get('shared_id');
const fetchMindMapFunc = sharedId
? searchService.mindmapShare
: chatService.getMindMap;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchMindMap'],
gcTime: 0,
mutationFn: async (params: IAskRequestBody) => {
try {
const ret = await fetchMindMapFunc(params);
return ret?.data?.data ?? {};
} catch (error: any) {
if (has(error, 'message')) {
message.error(error.message);
}
return [];
}
},
});
return { data, loading, fetchMindMap: mutateAsync };
};
export const useTestChunkRetrieval = (
tenantId?: string,
): ResponsePostType<ITestingResult> & {
testChunk: (...params: any[]) => void;
} => {
const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const retrievalTestFunc = shared_id
? kbService.retrievalTestShare
: kbService.retrieval_test;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testChunk'], // This method is invalid
gcTime: 0,
mutationFn: async (values: any) => {
const { data } = await retrievalTestFunc({
...values,
kb_id: values.kb_id ?? knowledgeBaseId,
page,
size: pageSize,
tenant_id: tenantId,
});
if (data.code === 0) {
const res = data.data;
return {
...res,
documents: res.doc_aggs,
};
}
return (
data?.data ?? {
chunks: [],
documents: [],
total: 0,
}
);
},
});
return {
data: data ?? { chunks: [], documents: [], total: 0 },
loading,
testChunk: mutateAsync,
};
};
export const useTestChunkAllRetrieval = (
tenantId?: string,
): ResponsePostType<ITestingResult> & {
testChunkAll: (...params: any[]) => void;
} => {
const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const retrievalTestFunc = shared_id
? kbService.retrievalTestShare
: kbService.retrieval_test;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testChunkAll'], // This method is invalid
gcTime: 0,
mutationFn: async (values: any) => {
const { data } = await retrievalTestFunc({
...values,
kb_id: values.kb_id ?? knowledgeBaseId,
doc_ids: [],
page,
size: pageSize,
tenant_id: tenantId,
});
if (data.code === 0) {
const res = data.data;
return {
...res,
documents: res.doc_aggs,
};
}
return (
data?.data ?? {
chunks: [],
documents: [],
total: 0,
}
);
},
});
return {
data: data ?? { chunks: [], documents: [], total: 0 },
loading,
testChunkAll: mutateAsync,
};
};
export const useTestRetrieval = (
kbIds: string[],
searchStr: string,
sendingLoading: boolean,
) => {
const { testChunk, loading } = useTestChunkRetrieval();
const { pagination } = useGetPaginationWithRouter();
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
const handleTestChunk = useCallback(() => {
const q = trim(searchStr);
if (sendingLoading || isEmpty(q)) return;
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: Array.isArray(selectedDocumentIds) ? selectedDocumentIds : [],
page: pagination.current,
size: pagination.pageSize,
});
}, [
sendingLoading,
searchStr,
kbIds,
testChunk,
selectedDocumentIds,
pagination,
]);
useEffect(() => {
handleTestChunk();
}, [handleTestChunk]);
return {
loading,
selectedDocumentIds,
setSelectedDocumentIds,
};
};
export const useFetchRelatedQuestions = (
tenantId?: string,
searchId?: string,
) => {
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const retrievalTestFunc = shared_id
? searchService.getRelatedQuestionsShare
: chatService.getRelatedQuestions;
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchRelatedQuestions'],
gcTime: 0,
mutationFn: async (question: string): Promise<string[]> => {
const { data } = await retrievalTestFunc({
question,
tenant_id: tenantId,
search_id: searchId,
});
return data?.data ?? [];
},
});
return { data, loading, fetchRelatedQuestions: mutateAsync };
};
export const useSendQuestion = (
kbIds: string[],
tenantId?: string,
searchId: string = '',
related_search: boolean = false,
) => {
const { sharedId } = useGetSharedSearchParams();
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
sharedId ? api.askShare : api.ask,
);
const { testChunk, loading } = useTestChunkRetrieval(tenantId);
const { testChunkAll } = useTestChunkAllRetrieval(tenantId);
const [sendingLoading, setSendingLoading] = useState(false);
const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer);
const { fetchRelatedQuestions, data: relatedQuestions } =
useFetchRelatedQuestions(tenantId, searchId);
const [searchStr, setSearchStr] = useState<string>('');
const [isFirstRender, setIsFirstRender] = useState(true);
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
const { pagination, setPagination } = useGetPaginationWithRouter();
const sendQuestion = useCallback(
(question: string, enableAI: boolean = true) => {
const q = trim(question);
if (isEmpty(q)) return;
setPagination({ page: 1 });
setIsFirstRender(false);
setCurrentAnswer({} as IAnswer);
if (enableAI) {
setSendingLoading(true);
send({ kb_ids: kbIds, question: q, tenantId, search_id: searchId });
}
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
page: 1,
size: pagination.pageSize,
search_id: searchId,
});
if (related_search) {
fetchRelatedQuestions(q);
}
},
[
send,
testChunk,
kbIds,
fetchRelatedQuestions,
setPagination,
pagination.pageSize,
tenantId,
searchId,
related_search,
],
);
const handleSearchStrChange: ChangeEventHandler<HTMLInputElement> =
useCallback((e) => {
setSearchStr(e.target.value);
}, []);
const handleClickRelatedQuestion = useCallback(
(question: string, enableAI: boolean = true) =>
() => {
if (sendingLoading) return;
setSearchStr(question);
sendQuestion(question, enableAI);
},
[sendQuestion, sendingLoading],
);
const handleTestChunk = useCallback(
(documentIds: string[], page: number = 1, size: number = 10) => {
const q = trim(searchStr);
if (sendingLoading || isEmpty(q)) return;
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: documentIds ?? selectedDocumentIds,
page,
size,
search_id: searchId,
});
testChunkAll({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: [],
page,
size,
search_id: searchId,
});
},
[
searchStr,
sendingLoading,
testChunk,
kbIds,
selectedDocumentIds,
testChunkAll,
searchId,
],
);
useEffect(() => {
if (!isEmpty(answer)) {
setCurrentAnswer(answer);
}
}, [answer]);
useEffect(() => {
if (done) {
setSendingLoading(false);
}
}, [done]);
return {
sendQuestion,
handleSearchStrChange,
handleClickRelatedQuestion,
handleTestChunk,
setSelectedDocumentIds,
loading,
sendingLoading,
answer: currentAnswer,
relatedQuestions: relatedQuestions?.slice(0, 5) ?? [],
searchStr,
setSearchStr,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty: isEmpty(trim(searchStr)),
stopOutputMessage,
};
};
export const useSearching = ({
searchText,
data: searchData,
setSearchText,
}: ISearchingProps) => {
const { tenantId } = useGetSharedSearchParams();
const {
sendQuestion,
handleClickRelatedQuestion,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
searchStr,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
setSearchStr,
stopOutputMessage,
} = useSendQuestion(
searchData.search_config.kb_ids,
tenantId as string,
searchData.id,
searchData.search_config.related_search,
);
const handleSearchStrChange = useCallback(
(value: string) => {
setSearchStr(value);
},
[setSearchStr],
);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useEffect(() => {
if (searchText) {
setSearchStr(searchText);
sendQuestion(searchText, searchData.search_config.summary);
setSearchText?.('');
}
}, [
searchText,
sendQuestion,
setSearchStr,
setSearchText,
searchData.search_config.summary,
]);
const {
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
} = useShowMindMapDrawer(
searchData.search_config.kb_ids,
searchStr,
searchData.id,
);
const { chunks, total } = useSelectTestingResult();
const handleSearch = useCallback(
(value: string) => {
sendQuestion(value, searchData.search_config.summary);
setSearchStr?.(value);
hideMindMapModal();
},
[
setSearchStr,
sendQuestion,
hideMindMapModal,
searchData.search_config.summary,
],
);
const { pagination, setPagination } = useGetPaginationWithRouter();
const onChange = (pageNumber: number, pageSize: number) => {
setPagination({ page: pageNumber, pageSize });
handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
};
return {
sendQuestion,
handleClickRelatedQuestion,
handleSearchStrChange,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
searchStr,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
setSearchStr,
stopOutputMessage,
visible,
hideModal,
documentId,
selectedChunk,
clickDocumentButton,
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
chunks,
total,
handleSearch,
pagination,
onChange,
};
};
export const useCheckSettings = (data: ISearchAppDetailProps) => {
if (!data) {
return {
openSetting: false,
};
}
const { search_config, name } = data;
const { kb_ids } = search_config;
return {
openSetting: kb_ids && kb_ids.length > 0 && name ? false : true,
};
};

View File

@@ -0,0 +1,116 @@
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 100%, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translate3d(-50%, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translate3d(50%, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeOutRight {
from {
opacity: 0;
transform: translate3d(0, 0, 0);
}
to {
opacity: 1;
transform: translate3d(120%, 0, 0);
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translate3d(0, -50%, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.animate-fade-in-up {
animation-name: fadeInUp;
animation-duration: 0.5s;
animation-fill-mode: both;
}
.animate-fade-in-down {
animation-name: fadeInDown;
animation-duration: 0.5s;
animation-fill-mode: both;
}
.animate-fade-in-left {
animation-name: fadeInLeft;
animation-duration: 0.5s;
animation-fill-mode: both;
}
.animate-fade-in-right {
animation-name: fadeInRight;
animation-duration: 0.5s;
animation-fill-mode: both;
}
.animate-fade-out-right {
animation-name: fadeOutRight;
animation-duration: 0.5s;
animation-fill-mode: both;
}
.delay-100 {
animation-delay: 0.1s;
}
.delay-200 {
animation-delay: 0.2s;
}
.delay-300 {
animation-delay: 0.3s;
}
.delay-400 {
animation-delay: 0.4s;
}
.delay-500 {
animation-delay: 0.5s;
}
.delay-600 {
animation-delay: 0.6s;
}
.delay-700 {
animation-delay: 0.7s;
}
.highlightContent {
.multipleLineEllipsis(2);
em {
color: red;
font-style: normal;
}
}

View File

@@ -0,0 +1,162 @@
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import { SharedFrom } from '@/constants/chat';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import {
useFetchTenantInfo,
useFetchUserInfo,
} from '@/hooks/user-setting-hooks';
import { Send, Settings } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
} from '../next-searches/hooks';
import EmbedAppModal from './embed-app-modal';
import { useCheckSettings } from './hooks';
import './index.less';
import SearchHome from './search-home';
import { SearchSetting } from './search-setting';
import SearchingPage from './searching';
export default function SearchPage() {
const { navigateToSearchList } = useNavigatePage();
const [isSearching, setIsSearching] = useState(false);
const { data: SearchData } = useFetchSearchDetail();
const { beta, handleOperate } = useFetchTokenListBeforeOtherStep();
const [openSetting, setOpenSetting] = useState(false);
const [openEmbed, setOpenEmbed] = useState(false);
const [searchText, setSearchText] = useState('');
const { data: tenantInfo } = useFetchTenantInfo();
const { data: userInfo } = useFetchUserInfo();
const tenantId = tenantInfo.tenant_id;
const { t } = useTranslation();
const { openSetting: checkOpenSetting } = useCheckSettings(
SearchData as ISearchAppDetailProps,
);
useEffect(() => {
setOpenSetting(checkOpenSetting);
}, [checkOpenSetting]);
useEffect(() => {
if (isSearching) {
setOpenSetting(false);
}
}, [isSearching]);
return (
<section>
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToSearchList}>
{t('header.search')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{SearchData?.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex gap-3 w-full bg-bg-base">
<div className="flex-1">
{!isSearching && (
<div className="animate-fade-in-down">
<SearchHome
setIsSearching={setIsSearching}
isSearching={isSearching}
searchText={searchText}
setSearchText={setSearchText}
userInfo={userInfo}
canSearch={!checkOpenSetting}
/>
</div>
)}
{isSearching && (
<div className="animate-fade-in-up">
<SearchingPage
setIsSearching={setIsSearching}
searchText={searchText}
setSearchText={setSearchText}
data={SearchData as ISearchAppDetailProps}
/>
</div>
)}
</div>
{openSetting && (
<SearchSetting
className="mt-20 mr-2"
open={openSetting}
setOpen={setOpenSetting}
data={SearchData as ISearchAppDetailProps}
/>
)}
{
<EmbedAppModal
open={openEmbed}
setOpen={setOpenEmbed}
url="/next-search/share"
token={SearchData?.id as string}
from={SharedFrom.Search}
tenantId={tenantId}
beta={beta}
/>
}
{
// <EmbedDialog
// visible={openEmbed}
// hideModal={setOpenEmbed}
// token={SearchData?.id as string}
// from={SharedFrom.Search}
// beta={beta}
// isAgent={false}
// ></EmbedDialog>
}
</div>
<div className="absolute right-5 top-4 ">
<Button
className="bg-text-primary text-bg-base border-b-accent-primary border-b-2"
onClick={() => {
handleOperate().then((res) => {
console.log(res, 'res');
if (res) {
setOpenEmbed(!openEmbed);
}
});
}}
>
<Send />
<div>{t('search.embedApp')}</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">
{t('search.searchSettings')}
</div>
</Button>
</div>
)}
</section>
);
}

View File

@@ -0,0 +1,298 @@
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 { memo, 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, {
ADD_TAGS: ['think', 'section'],
ADD_ATTR: ['class'],
});
// 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 {
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 { 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 className="!w-fit">
{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 memo(MarkdownContent);

View File

@@ -0,0 +1,50 @@
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, loading }: IProps) => {
const { t } = useTranslation();
const percent = usePendingMindMap();
return (
<div className="w-full h-full">
<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=" rounded-lg p-4 w-full h-full">
<Progress value={percent} className="h-1 flex-1 min-w-10" />
</div>
)}
{!loading && (
<div className="bg-bg-card rounded-lg p-4 w-full h-full">
<IndentedTree
data={data}
show
style={{
width: '100%',
height: '100%',
}}
></IndentedTree>
</div>
)}
</div>
);
};
export default MindMapDrawer;

View File

@@ -0,0 +1,11 @@
.selectFilesCollapse {
:global(.ant-collapse-header) {
padding-left: 22px;
}
margin-bottom: 32px;
overflow-y: auto;
}
.selectFilesTitle {
padding-right: 10px;
}

View File

@@ -0,0 +1,234 @@
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 { Separator } from '@/components/ui/separator';
import {
useAllTestingResult,
useSelectTestingResult,
} from '@/hooks/knowledge-hooks';
import { cn } from '@/lib/utils';
import { CheckIcon, ChevronDown, Files, XIcon } from 'lucide-react';
import { useMemo, useState } from 'react';
interface IProps {
onTesting(documentIds: string[]): void;
setSelectedDocumentIds(documentIds: string[]): void;
selectedDocumentIds: string[];
}
const RetrievalDocuments = ({
onTesting,
selectedDocumentIds,
setSelectedDocumentIds,
}: IProps) => {
const { documents: documentsAll } = useAllTestingResult();
const { documents } = useSelectTestingResult();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
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;

View File

@@ -0,0 +1,96 @@
import { Input } from '@/components/originui/input';
import Spotlight from '@/components/spotlight';
import message from '@/components/ui/message';
import { IUserInfo } from '@/interfaces/database/user-setting';
import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import './index.less';
export default function SearchPage({
isSearching,
setIsSearching,
searchText,
setSearchText,
userInfo,
canSearch,
}: {
isSearching: boolean;
setIsSearching: Dispatch<SetStateAction<boolean>>;
searchText: string;
setSearchText: Dispatch<SetStateAction<string>>;
userInfo?: IUserInfo;
canSearch?: boolean;
}) {
// const { data: userInfo } = useFetchUserInfo();
const { t } = useTranslation();
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]">
<h1
className={cn(
'text-4xl font-bold bg-gradient-to-l from-[#40EBE3] to-[#4A51FF] bg-clip-text',
)}
>
RAGFlow
</h1>
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[230px] border">
{!isSearching && <Spotlight className="z-0" />}
<div className="flex flex-col justify-center items-center w-2/3">
{!isSearching && (
<>
<p className="mb-4 transition-opacity">👋 Hi there</p>
<p className="mb-10 transition-opacity">
{userInfo && (
<>
{t('search.welcomeBack')}, {userInfo.nickname}
</>
)}
</p>
</>
)}
<div className="relative w-full ">
<Input
placeholder={t('search.searchGreeting')}
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') {
if (canSearch === false) {
message.warning(t('search.chooseDataset'));
return;
}
setIsSearching(!isSearching);
}
}}
onChange={(e) => {
if (canSearch === false) {
message.warning(t('search.chooseDataset'));
return;
}
setSearchText(e.target.value || '');
}}
/>
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 transform rounded-full bg-text-primary p-2 text-bg-base shadow w-12"
onClick={() => {
if (canSearch === false) {
message.warning(t('search.chooseDataset'));
return;
}
setIsSearching(!isSearching);
}}
>
<Search size={22} className="m-auto" />
</button>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,236 @@
import { SliderInputSwitchFormField } from '@/components/llm-setting-items/slider';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import { camelCase, isEqual } from 'lodash';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
interface LlmSettingFieldItemsProps {
prefix?: string;
options?: any[];
}
const LlmSettingEnableSchema = {
temperatureEnabled: z.boolean(),
topPEnabled: z.boolean(),
presencePenaltyEnabled: z.boolean(),
frequencyPenaltyEnabled: z.boolean(),
};
export const LlmSettingSchema = {
llm_id: z.string(),
parameter: z.string().optional(),
temperature: z.coerce.number().optional(),
top_p: z.coerce.number().optional(),
presence_penalty: z.coerce.number().optional(),
frequency_penalty: z.coerce.number().optional(),
...LlmSettingEnableSchema,
// maxTokensEnabled: z.boolean(),
};
export function LlmSettingFieldItems({
prefix,
options,
}: LlmSettingFieldItemsProps) {
const form = useFormContext();
const { t } = useTranslate('chat');
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const handleChange = useCallback(
(parameter: string) => {
const values =
settledModelVariableMap[
parameter as keyof typeof settledModelVariableMap
];
const enabledKeys = Object.keys(LlmSettingEnableSchema);
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const element = values[key as keyof typeof values];
form.setValue(`${prefix}.${key}`, element);
}
}
if (enabledKeys && enabledKeys.length) {
for (const key of enabledKeys) {
form.setValue(`${prefix}.${key}`, true);
}
}
},
[form, prefix],
);
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: t(camelCase(x)),
value: x,
})) as unknown as { label: string; value: ModelVariableType | 'Custom' }[];
parameterOptions.push({
label: t(camelCase('Custom')),
value: 'Custom',
});
const getFieldWithPrefix = useCallback(
(name: string) => {
return prefix ? `${prefix}.${name}` : name;
},
[prefix],
);
const checkParameterIsEquel = () => {
const [
parameter,
topPValue,
frequencyPenaltyValue,
temperatureValue,
presencePenaltyValue,
] = form.getValues([
getFieldWithPrefix('parameter'),
getFieldWithPrefix('temperature'),
getFieldWithPrefix('top_p'),
getFieldWithPrefix('frequency_penalty'),
getFieldWithPrefix('presence_penalty'),
]);
if (parameter && parameter !== 'Custom') {
const parameterValue =
settledModelVariableMap[parameter as keyof typeof ModelVariableType];
const parameterRealValue = {
top_p: topPValue,
temperature: temperatureValue,
frequency_penalty: frequencyPenaltyValue,
presence_penalty: presencePenaltyValue,
};
if (!isEqual(parameterValue, parameterRealValue)) {
form.setValue(getFieldWithPrefix('parameter'), 'Custom');
}
}
};
return (
<div className="space-y-5">
<FormField
control={form.control}
name={getFieldWithPrefix('llm_id')}
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('model')}
</FormLabel>
<FormControl>
<SelectWithSearch
options={options || modelOptions}
triggerClassName="!bg-bg-input"
{...field}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={getFieldWithPrefix('parameter')}
render={({ field }) => (
<FormItem className="flex justify-between gap-4 items-center">
<FormLabel>{t('freedom')}</FormLabel>
<FormControl>
<div className="w-28">
<Select
{...field}
onValueChange={(val) => {
handleChange(val);
field.onChange(val);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{parameterOptions.map((x) => (
<SelectItem value={x.value} key={x.value}>
{x.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName={getFieldWithPrefix('temperatureEnabled')}
label="temperature"
max={1}
min={0}
step={0.01}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName={getFieldWithPrefix('topPEnabled')}
label="topP"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName={getFieldWithPrefix('presencePenaltyEnabled')}
label="presencePenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName={getFieldWithPrefix('frequencyPenaltyEnabled')}
label="frequencyPenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEquel();
}}
></SliderInputSwitchFormField>
{/* <SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
label="maxTokens"
max={128000}
></SliderInputSwitchFormField> */}
</div>
);
}

View File

@@ -0,0 +1,651 @@
// src/pages/next-search/search-setting.tsx
import { AvatarUpload } from '@/components/avatar-upload';
import {
MetadataFilter,
MetadataFilterSchema,
} from '@/components/metadata-filter';
import { Button } from '@/components/ui/button';
import { SingleFormSlider } from '@/components/ui/dual-range-slider';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
MultiSelect,
MultiSelectOptionType,
} from '@/components/ui/multi-select';
import { RAGFlowSelect } from '@/components/ui/select';
import { Spin } from '@/components/ui/spin';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import {
useComposeLlmOptionsByModelTypes,
useSelectLlmOptionsByModelType,
} from '@/hooks/llm-hooks';
import { useFetchTenantInfo } from '@/hooks/user-setting-hooks';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { X } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { LlmModelType } from '../dataset/dataset/constant';
import {
ISearchAppDetailProps,
IUpdateSearchProps,
IllmSettingProps,
useUpdateSearch,
} from '../next-searches/hooks';
import {
LlmSettingFieldItems,
LlmSettingSchema,
} from './search-setting-aisummery-config';
interface SearchSettingProps {
open: boolean;
setOpen: (open: boolean) => void;
className?: string;
data: ISearchAppDetailProps;
}
const SearchSettingFormSchema = z
.object({
search_id: z.string().optional(),
name: z.string().min(1, 'Name is required'),
avatar: z.string().optional(),
description: z.string().optional(),
search_config: z.object({
kb_ids: z.array(z.string()).min(1, 'At least one dataset is required'),
vector_similarity_weight: z.number().min(0).max(1),
web_search: z.boolean(),
similarity_threshold: z.number(),
use_kg: z.boolean(),
rerank_id: z.string(),
use_rerank: z.boolean(),
top_k: z.number(),
summary: z.boolean(),
llm_setting: z.object(LlmSettingSchema),
related_search: z.boolean(),
query_mindmap: z.boolean(),
...MetadataFilterSchema,
}),
})
.superRefine((data, ctx) => {
if (data.search_config.use_rerank && !data.search_config.rerank_id) {
ctx.addIssue({
path: ['search_config', 'rerank_id'],
message: 'Rerank model is required when rerank is enabled',
code: z.ZodIssueCode.custom,
});
}
if (data.search_config.summary && !data.search_config.llm_setting?.llm_id) {
ctx.addIssue({
path: ['search_config', 'llm_setting', 'llm_id'],
message: 'Model is required when AI Summary is enabled',
code: z.ZodIssueCode.custom,
});
}
});
type SearchSettingFormData = z.infer<typeof SearchSettingFormSchema>;
const SearchSetting: React.FC<SearchSettingProps> = ({
open = false,
setOpen,
className,
data,
}) => {
const [width0, setWidth0] = useState('w-[440px]');
const { search_config } = data || {};
const { llm_setting } = search_config || {};
const formMethods = useForm<SearchSettingFormData>({
resolver: zodResolver(SearchSettingFormSchema),
});
const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]);
const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState('');
const { t } = useTranslation();
const descriptionDefaultValue = t('search.descriptionValue');
const resetForm = useCallback(() => {
formMethods.reset({
search_id: data?.id,
name: data?.name || '',
avatar: data?.avatar || '',
description: data?.description || descriptionDefaultValue,
search_config: {
kb_ids: search_config?.kb_ids || [],
vector_similarity_weight:
(search_config?.vector_similarity_weight
? 1 - search_config?.vector_similarity_weight
: 0.3) || 0.3,
web_search: search_config?.web_search || false,
doc_ids: [],
similarity_threshold: search_config?.similarity_threshold || 0.2,
use_kg: false,
rerank_id: search_config?.rerank_id || '',
use_rerank: search_config?.rerank_id ? true : false,
top_k: search_config?.top_k || 1024,
summary: search_config?.summary || false,
chat_id: search_config?.chat_id || '',
llm_setting: {
llm_id: search_config?.chat_id || '',
parameter: llm_setting?.parameter,
temperature: llm_setting?.temperature || 0,
top_p: llm_setting?.top_p || 0,
frequency_penalty: llm_setting?.frequency_penalty || 0,
presence_penalty: llm_setting?.presence_penalty || 0,
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,
keyword: false,
related_search: search_config?.related_search || false,
query_mindmap: search_config?.query_mindmap || false,
meta_data_filter: search_config?.meta_data_filter,
},
});
}, [data, search_config, llm_setting, formMethods, descriptionDefaultValue]);
useEffect(() => {
resetForm();
}, [resetForm]);
useEffect(() => {
if (!open) {
setTimeout(() => {
setWidth0('w-0 hidden');
}, 500);
} else {
setWidth0('w-[440px]');
}
}, [open]);
const { list: datasetListOrigin } = useFetchKnowledgeList();
useEffect(() => {
const datasetListMap = datasetListOrigin.map((item: IKnowledge) => {
return {
label: item.name,
suffix: (
<div className="text-xs px-4 p-1 bg-bg-card text-text-secondary rounded-lg border border-bg-card">
{item.embd_id}
</div>
),
value: item.id,
disabled:
item.embd_id !== datasetSelectEmbdId && datasetSelectEmbdId !== '',
};
});
setDatasetList(datasetListMap);
}, [datasetListOrigin, datasetSelectEmbdId]);
const handleDatasetSelectChange = (
value: string[],
onChange: (value: string[]) => void,
) => {
console.log(value);
if (value.length) {
const data = datasetListOrigin?.find((item) => item.id === value[0]);
setDatasetSelectEmbdId(data?.embd_id ?? '');
} else {
setDatasetSelectEmbdId('');
}
formMethods.setValue('search_config.kb_ids', value);
onChange?.(value);
};
const allOptions = useSelectLlmOptionsByModelType();
const rerankModelOptions = useMemo(() => {
return allOptions[LlmModelType.Rerank];
}, [allOptions]);
const aiSummeryModelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const rerankModelDisabled = useWatch({
control: formMethods.control,
name: 'search_config.use_rerank',
});
const aiSummaryDisabled = useWatch({
control: formMethods.control,
name: 'search_config.summary',
});
const { updateSearch } = useUpdateSearch();
const [formSubmitLoading, setFormSubmitLoading] = useState(false);
const { data: systemSetting } = useFetchTenantInfo();
const onSubmit = async (
formData: IUpdateSearchProps & { tenant_id: string },
) => {
try {
setFormSubmitLoading(true);
const { search_config, ...other_formdata } = formData;
const {
llm_setting,
vector_similarity_weight,
use_rerank,
rerank_id,
...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 IllmSettingProps;
await updateSearch({
...other_formdata,
search_config: {
...other_config,
chat_id: llm_setting.llm_id,
vector_similarity_weight: 1 - vector_similarity_weight,
rerank_id: use_rerank ? rerank_id : '',
llm_setting: { ...llmSetting },
},
tenant_id: systemSetting.tenant_id,
});
setOpen(false);
} catch (error) {
console.error('Failed to update search:', error);
} finally {
setFormSubmitLoading(false);
}
};
return (
<div
className={cn(
'text-text-primary border p-4 pb-12 rounded-lg',
{
'animate-fade-in-right': open,
'animate-fade-out-right': !open,
},
width0,
className,
)}
style={{ maxHeight: 'calc(100dvh - 170px)' }}
>
<div className="flex justify-between items-center text-base mb-8">
<div className="text-text-primary">{t('search.searchSettings')}</div>
<div onClick={() => setOpen(false)}>
<X size={16} className="text-text-primary cursor-pointer" />
</div>
</div>
<div
style={{ maxHeight: 'calc(100dvh - 270px)' }}
className="overflow-y-auto scrollbar-auto p-1 text-text-secondary"
>
<Form {...formMethods}>
<form
onSubmit={formMethods.handleSubmit(
(data) => {
console.log('Form submitted with data:', data);
onSubmit(data as unknown as IUpdateSearchProps);
},
(errors) => {
console.log('Validation errors:', errors);
},
)}
className="space-y-6"
>
{/* Name */}
<FormField
control={formMethods.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('search.name')}
</FormLabel>
<FormControl>
<Input placeholder={t('search.name')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Avatar */}
<FormField
control={formMethods.control}
name="avatar"
render={({ field }) => (
<FormItem>
<FormLabel>{t('search.avatar')}</FormLabel>
<FormControl>
<AvatarUpload {...field}></AvatarUpload>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Description */}
<FormField
control={formMethods.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('search.description')}</FormLabel>
<FormControl>
<Textarea
placeholder={descriptionDefaultValue}
{...field}
onFocus={() => {
if (field.value === descriptionDefaultValue) {
field.onChange('');
}
}}
onBlur={() => {
if (field.value === '') {
field.onChange(descriptionDefaultValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Datasets */}
<FormField
control={formMethods.control}
name="search_config.kb_ids"
rules={{ required: 'Datasets is required' }}
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('search.datasets')}
</FormLabel>
<FormControl className="bg-bg-input">
<MultiSelect
options={datasetList}
onValueChange={(value) => {
handleDatasetSelectChange(value, field.onChange);
}}
showSelectAll={false}
placeholder={t('chat.knowledgeBasesMessage')}
maxCount={10}
defaultValue={field.value}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<MetadataFilter prefix="search_config."></MetadataFilter>
<FormField
control={formMethods.control}
name="search_config.similarity_threshold"
render={({ field }) => (
<FormItem>
<FormLabel
tooltip={t('knowledgeDetails.similarityThresholdTip')}
>
{t('knowledgeDetails.similarityThreshold')}
</FormLabel>
<div
className={cn(
'flex items-center gap-4 justify-between',
className,
)}
>
<FormControl>
<SingleFormSlider
{...field}
max={1}
min={0}
step={0.01}
></SingleFormSlider>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={1}
min={0}
step={0.01}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Keyword Similarity Weight */}
<FormField
control={formMethods.control}
name="search_config.vector_similarity_weight"
render={({ field }) => (
<FormItem>
<FormLabel
tooltip={t('knowledgeDetails.vectorSimilarityWeightTip')}
>
<span className="text-destructive mr-1"> *</span>
{t('knowledgeDetails.vectorSimilarityWeight')}
</FormLabel>
<div
className={cn(
'flex items-center gap-4 justify-between',
className,
)}
>
<FormControl>
<SingleFormSlider
{...field}
max={1}
min={0}
step={0.01}
></SingleFormSlider>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={1}
min={0}
step={0.01}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Rerank Model */}
<FormField
control={formMethods.control}
name="search_config.use_rerank"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>{t('search.rerankModel')}</FormLabel>
</FormItem>
)}
/>
{rerankModelDisabled && (
<>
<FormField
control={formMethods.control}
name={'search_config.rerank_id'}
// rules={{ required: 'Model is required' }}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('chat.model')}
</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={rerankModelOptions}
triggerClassName={'bg-bg-input'}
// disabled={disabled}
placeholder={t('chat.model')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={formMethods.control}
name="search_config.top_k"
render={({ field }) => (
<FormItem>
<FormLabel>Top K</FormLabel>
<div
className={cn(
'flex items-center gap-4 justify-between',
className,
)}
>
<FormControl>
<SingleFormSlider
{...field}
max={2048}
min={0}
step={1}
></SingleFormSlider>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={2048}
min={0}
step={1}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{/* AI Summary */}
<FormField
control={formMethods.control}
name="search_config.summary"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>{t('search.AISummary')}</FormLabel>
</FormItem>
)}
/>
{aiSummaryDisabled && (
<LlmSettingFieldItems
prefix="search_config.llm_setting"
options={aiSummeryModelOptions}
></LlmSettingFieldItems>
)}
{/* Feature Controls */}
{/* <FormField
control={formMethods.control}
name="search_config.web_search"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>{t('search.enableWebSearch')}</FormLabel>
</FormItem>
)}
/> */}
<FormField
control={formMethods.control}
name="search_config.related_search"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>{t('search.enableRelatedSearch')}</FormLabel>
</FormItem>
)}
/>
<FormField
control={formMethods.control}
name="search_config.query_mindmap"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>{t('search.showQueryMindmap')}</FormLabel>
</FormItem>
)}
/>
{/* Submit Button */}
<div className="flex justify-end"></div>
<div className="flex justify-end gap-2 absolute bottom-1 right-3 bg-bg-base w-[calc(100%-1em)] py-2">
<Button
type="reset"
variant={'transparent'}
onClick={() => {
resetForm();
setOpen(false);
}}
>
{t('search.cancelText')}
</Button>
<Button type="submit" disabled={formSubmitLoading}>
{formSubmitLoading && (
<div className="size-4">
<Spin size="small" />
</div>
)}
{t('search.okText')}
</Button>
</div>
</form>
</Form>
</div>
</div>
);
};
export { SearchSetting };

View File

@@ -0,0 +1,315 @@
import { FileIcon } from '@/components/icon-font';
import { ImageWithPopover } from '@/components/image';
import { Input } from '@/components/originui/input';
import { SkeletonCard } from '@/components/skeleton-card';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { IReference } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import DOMPurify from 'dompurify';
import { isEmpty } from 'lodash';
import { BrainCircuit, Search, X } from 'lucide-react';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import PdfDrawer from './document-preview-modal';
import HightLightMarkdown from './highlight-markdown';
import { ISearchReturnProps } from './hooks';
import './index.less';
import MarkdownContent from './markdown-content';
import MindMapDrawer from './mindmap-drawer';
import RetrievalDocuments from './retrieval-documents';
export default function SearchingView({
setIsSearching,
searchData,
handleClickRelatedQuestion,
handleTestChunk,
setSelectedDocumentIds,
answer,
sendingLoading,
relatedQuestions,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
searchStr,
stopOutputMessage,
visible,
hideModal,
documentId,
selectedChunk,
clickDocumentButton,
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
chunks,
total,
handleSearch,
pagination,
onChange,
}: ISearchReturnProps & {
setIsSearching?: Dispatch<SetStateAction<boolean>>;
searchData: ISearchAppDetailProps;
}) {
const { t } = useTranslation();
// useEffect(() => {
// const changeLanguage = async () => {
// await i18n.changeLanguage('zh');
// };
// changeLanguage();
// }, [i18n]);
const [searchtext, setSearchtext] = useState<string>('');
useEffect(() => {
setSearchtext(searchStr);
}, [searchStr, setSearchtext]);
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',
)}
>
<h1
className={cn(
'text-4xl font-bold bg-gradient-to-l from-[#40EBE3] to-[#4A51FF] bg-clip-text cursor-pointer',
)}
onClick={() => {
setIsSearching?.(false);
}}
>
RAGFlow
</h1>
<div
className={cn(
' 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
placeholder={t('search.searchGreeting')}
className={cn(
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-bg-base',
)}
value={searchtext}
onChange={(e) => {
setSearchtext(e.target.value);
}}
disabled={sendingLoading}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch(searchtext);
}
}}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
<X
className="text-text-secondary cursor-pointer"
size={14}
onClick={() => {
setSearchtext('');
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary ml-4">|</span>
<button
type="button"
className="rounded-full bg-text-primary p-1 text-bg-base shadow w-12 h-8 ml-4"
onClick={() => {
if (sendingLoading) {
stopOutputMessage();
} else {
handleSearch(searchtext);
}
}}
>
{sendingLoading ? (
// <Square size={22} className="m-auto" />
<div className="w-2 h-2 bg-bg-base m-auto"></div>
) : (
<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 && !isSearchStrEmpty && (
<>
<div className="flex justify-start items-start text-text-primary text-2xl">
{t('search.AISummary')}
</div>
{isEmpty(answer) && sendingLoading ? (
<SkeletonCard className=" mt-2" />
) : (
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 */}
{!isSearchStrEmpty && (
<>
<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 ">
{chunks?.length > 0 && (
<>
{chunks.map((chunk, index) => {
return (
<div key={index}>
<div className="w-full flex flex-col">
<div className="w-full highlightContent">
<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 !w-full max-w-lg ">
<div className="max-h-96 overflow-auto scrollbar-thin">
<HightLightMarkdown>
{chunk.content_with_weight}
</HightLightMarkdown>
</div>
</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>
)}
</div>
);
})}
</>
)}
{relatedQuestions?.length > 0 &&
searchData.search_config.related_search && (
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">
{t('search.relatedSearch')}
</p>
<div className="mt-2 flex flex-wrap justify-start gap-2">
{relatedQuestions?.map((x, idx) => (
<Button
key={idx}
variant="transparent"
className="bg-bg-card text-text-secondary"
onClick={handleClickRelatedQuestion(
x,
searchData.search_config.summary,
)}
>
{x}
</Button>
))}
</div>
</div>
)}
</div>
</div>
{total > 0 && (
<div className="mt-8 px-8 pb-8 text-base">
<RAGFlowPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={total}
onChange={onChange}
></RAGFlowPagination>
</div>
)}
</div>
{mindMapVisible && (
<div className="flex-1 h-[88dvh] z-30 ml-32 mt-5">
<MindMapDrawer
visible={mindMapVisible}
hideModal={hideMindMapModal}
data={mindMap}
loading={mindMapLoading}
></MindMapDrawer>
</div>
)}
</div>
{!mindMapVisible &&
!isFirstRender &&
!isSearchStrEmpty &&
!isEmpty(searchData.search_config.kb_ids) &&
searchData.search_config.query_mindmap && (
<Popover>
<PopoverTrigger asChild>
<div
className="rounded-lg h-16 w-16 p-0 absolute top-28 right-3 z-30 border cursor-pointer flex justify-center items-center bg-bg-card"
onClick={showMindMapModal}
>
{/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */}
<BrainCircuit size={36} />
</div>
</PopoverTrigger>
<PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent>
</Popover>
)}
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
</section>
);
}

View File

@@ -0,0 +1,30 @@
import { Dispatch, SetStateAction } from 'react';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import { useSearching } from './hooks';
import './index.less';
import SearchingView from './search-view';
export default function SearchingPage({
searchText,
data: searchData,
setIsSearching,
setSearchText,
}: {
searchText: string;
setIsSearching: Dispatch<SetStateAction<boolean>>;
setSearchText: Dispatch<SetStateAction<string>>;
data: ISearchAppDetailProps;
}) {
const searchingParam = useSearching({
searchText,
data: searchData,
setIsSearching,
setSearchText,
});
return (
<SearchingView
{...searchingParam}
searchData={searchData}
setIsSearching={setIsSearching}
/>
);
}

View File

@@ -0,0 +1,66 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import i18n from '@/locales/config';
import { useEffect, useState } from 'react';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
} from '../../next-searches/hooks';
import { useCheckSettings, useGetSharedSearchParams } from '../hooks';
import '../index.less';
import SearchHome from '../search-home';
import SearchingPage from '../searching';
export default function ShareSeachPage() {
const { tenantId, locale, visibleAvatar } = useGetSharedSearchParams();
const {
data: searchData = {
search_config: { kb_ids: [] },
} as unknown as ISearchAppDetailProps,
} = useFetchSearchDetail(tenantId as string);
const [isSearching, setIsSearching] = useState(false);
const [searchText, setSearchText] = useState('');
const { openSetting: canSearch } = useCheckSettings(
searchData as ISearchAppDetailProps,
);
useEffect(() => {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale]);
return (
<>
{visibleAvatar && (
<div className="flex justify-start items-center gap-2 mx-6 mt-6 text-text-primary">
<RAGFlowAvatar
className="size-6"
avatar={searchData.avatar}
name={searchData.name}
></RAGFlowAvatar>
<div>{searchData.name}</div>
</div>
)}
{/* <SearchingView {...searchingParam} searchData={searchData} />; */}
{!isSearching && (
<div className="animate-fade-in-down">
<SearchHome
setIsSearching={setIsSearching}
isSearching={isSearching}
searchText={searchText}
setSearchText={setSearchText}
canSearch={!canSearch}
/>
</div>
)}
{isSearching && (
<div className="animate-fade-in-up">
<SearchingPage
setIsSearching={setIsSearching}
searchText={searchText}
setSearchText={setSearchText}
data={searchData as ISearchAppDetailProps}
/>
</div>
)}
</>
);
}