Преглед на файлове

feat(search): Added app embedding functionality and optimized search page #3221 (#9499)

### What problem does this PR solve?
feat(search): Added app embedding functionality and optimized search
page #3221

- Added an Embed App button and related functionality
- Optimized the layout and interaction of the search settings interface
- Adjusted the search result display method
- Refactored some code to support new features
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
tags/v0.20.2
chanx преди 2 месеца
родител
ревизия
f9e5caa8ed
No account linked to committer's email address

+ 1
- 0
web/src/constants/chat.ts Целия файл

@@ -22,6 +22,7 @@ export const variableEnabledFieldMap = {
export enum SharedFrom {
Agent = 'agent',
Chat = 'chat',
Search = 'search',
}

export enum ChatSearchParams {

+ 5
- 2
web/src/hooks/use-agent-request.ts Целия файл

@@ -25,7 +25,7 @@ import { useDebounce } from 'ahooks';
import { get, set } from 'lodash';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useParams, useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';
import {
useGetPaginationWithRouter,
@@ -304,6 +304,9 @@ export const useSetAgent = (showMessage: boolean = true) => {
// Only one file can be uploaded at a time
export const useUploadCanvasFile = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const canvasId = id || shared_id;
const {
data,
isPending: loading,
@@ -321,7 +324,7 @@ export const useUploadCanvasFile = () => {
}

const { data } = await agentService.uploadCanvasFile(
{ url: api.uploadAgentFile(id), data: nextBody },
{ url: api.uploadAgentFile(canvasId as string), data: nextBody },
true,
);
if (data?.code === 0) {

+ 1
- 0
web/src/locales/en.ts Целия файл

@@ -1418,6 +1418,7 @@ This delimiter is used to split the input text into several text pieces echo of
},
search: {
createSearch: 'Create Search',
searchGreeting: 'How can I help you today ?',
},
},
};

+ 8
- 4
web/src/locales/zh-traditional.ts Целия файл

@@ -1185,9 +1185,13 @@ export default {
knowledge: '知識',
chat: '聊天',
},
},
modal: {
okText: '確認',
cancelText: '取消',
modal: {
okText: '確認',
cancelText: '取消',
},
search: {
createSearch: '新建查詢',
searchGreeting: '今天我能為你做些什麽?',
},
},
};

+ 8
- 7
web/src/locales/zh.ts Целия файл

@@ -1316,12 +1316,13 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
},
},
},
},
modal: {
okText: '确认',
cancelText: '取消',
},
search: {
createSearch: '新建查询',
modal: {
okText: '确认',
cancelText: '取消',
},
search: {
createSearch: '新建查询',
searchGreeting: '今天我能为你做些什么?',
},
},
};

+ 1
- 1
web/src/pages/agent/hooks/use-show-dialog.ts Целия файл

@@ -85,7 +85,7 @@ const getUrlWithToken = (token: string, from: string = 'chat') => {
return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`;
};

const useFetchTokenListBeforeOtherStep = () => {
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();


+ 9
- 7
web/src/pages/next-search/document-preview-modal/index.tsx Целия файл

@@ -12,7 +12,8 @@ import { useEffect, useState } from 'react';

interface IProps extends IModalProps<any> {
documentId: string;
chunk: IChunk | IReferenceChunk;
chunk: IChunk &
IReferenceChunk & { docnm_kwd: string; document_name: string };
}
function getFileExtensionRegex(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
@@ -30,21 +31,22 @@ const PdfDrawer = ({
// 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);
if (chunk.docnm_kwd || chunk.document_name) {
const type = getFileExtensionRegex(
chunk.docnm_kwd || chunk.document_name,
);
setFileType(type);
}
}, [chunk.docnm_kwd]);
}, [chunk.docnm_kwd, chunk.document_name]);
return (
<Modal
title={
<div className="flex items-center gap-2">
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
<FileIcon name={chunk.docnm_kwd || chunk.document_name}></FileIcon>
{chunk.docnm_kwd || chunk.document_name}
</div>
}
onCancel={hideModal}

+ 140
- 0
web/src/pages/next-search/embed-app-modal.tsx Целия файл

@@ -0,0 +1,140 @@
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 { useCallback, useMemo, useState } from 'react';

type IEmbedAppModalProps = {
open: any;
url: string;
token: string;
from: string;
beta: string;
setOpen: (e: any) => void;
tenantId: string;
};

const EmbedAppModal = (props: IEmbedAppModalProps) => {
const { t } = useTranslate('chat');
const { open, setOpen, token = '', from, beta = '', url, tenantId } = 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('avatarHidden')}
</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">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">Embed code</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">ID</label>
<div className="flex items-center">
<input
type="text"
value={token}
readOnly
className="flex-1 px-4 py-2 border border-gray-700 rounded-lg bg-bg-base focus:outline-none"
/>
<button
type="button"
onClick={() => navigator.clipboard.writeText(token)}
className="ml-2 p-2 text-gray-400 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;

+ 488
- 0
web/src/pages/next-search/hooks.ts Целия файл

@@ -0,0 +1,488 @@
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) => {
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,
});

return data?.data ?? [];
},
});

return { data, loading, fetchRelatedQuestions: mutateAsync };
};

export const useSendQuestion = (kbIds: string[], tenantId?: string) => {
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);
const [searchStr, setSearchStr] = useState<string>('');
const [isFirstRender, setIsFirstRender] = useState(true);
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);

const { pagination, setPagination } = useGetPaginationWithRouter();

const sendQuestion = useCallback(
(question: string) => {
const q = trim(question);
if (isEmpty(q)) return;
setPagination({ page: 1 });
setIsFirstRender(false);
setCurrentAnswer({} as IAnswer);
setSendingLoading(true);
send({ kb_ids: kbIds, question: q, tenantId });
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
page: 1,
size: pagination.pageSize,
});

fetchRelatedQuestions(q);
},
[
send,
testChunk,
kbIds,
fetchRelatedQuestions,
setPagination,
pagination.pageSize,
tenantId,
],
);

const handleSearchStrChange: ChangeEventHandler<HTMLInputElement> =
useCallback((e) => {
setSearchStr(e.target.value);
}, []);

const handleClickRelatedQuestion = useCallback(
(question: string) => () => {
if (sendingLoading) return;

setSearchStr(question);
sendQuestion(question);
},
[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,
});

testChunkAll({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: [],
page,
size,
});
},
[
searchStr,
sendingLoading,
testChunk,
kbIds,
selectedDocumentIds,
testChunkAll,
],
);

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

const handleSearchStrChange = useCallback(
(value: string) => {
console.log('handleSearchStrChange', value);
setSearchStr(value);
},
[setSearchStr],
);

const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();

useEffect(() => {
if (searchText) {
setSearchStr(searchText);
sendQuestion(searchText);
setSearchText?.('');
}
}, [searchText, sendQuestion, setSearchStr, setSearchText]);

const {
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
} = useShowMindMapDrawer(searchData.search_config.kb_ids, searchStr);
const { chunks, total } = useSelectTestingResult();

const handleSearch = useCallback(
(value: string) => {
sendQuestion(value);
setSearchStr?.(value);
},
[setSearchStr, sendQuestion],
);

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

+ 42
- 3
web/src/pages/next-search/index.tsx Целия файл

@@ -8,13 +8,17 @@ import {
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 { Settings } from 'lucide-react';
import { useFetchTenantInfo } from '@/hooks/user-setting-hooks';
import { Send, Settings } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useFetchTokenListBeforeOtherStep } from '../agent/hooks/use-show-dialog';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
} from '../next-searches/hooks';
import EmbedAppModal from './embed-app-modal';
import './index.less';
import SearchHome from './search-home';
import { SearchSetting } from './search-setting';
@@ -24,9 +28,15 @@ 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 tenantId = tenantInfo.tenant_id;
useEffect(() => {
handleOperate();
}, [handleOperate]);
useEffect(() => {
if (isSearching) {
setOpenSetting(false);
@@ -81,8 +91,37 @@ export default function SearchPage() {
data={SearchData as ISearchAppDetailProps}
/>
)}
{
<EmbedAppModal
open={openEmbed}
setOpen={setOpenEmbed}
url="/next-search/share"
token={SearchData?.id as string}
from={SharedFrom.Search}
beta={beta}
tenantId={tenantId}
/>
}
{
// <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-12 ">
<Button
className="bg-text-primary text-bg-base border-b-[#00BEB4] border-b-2"
onClick={() => setOpenEmbed(!openEmbed)}
>
<Send />
<div>Embed App</div>
</Button>
</div>

{!isSearching && (
<div className="absolute left-5 bottom-12 ">
<Button

+ 6
- 3
web/src/pages/next-search/mindmap-drawer.tsx Целия файл

@@ -13,7 +13,7 @@ const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => {
const { t } = useTranslation();
const percent = usePendingMindMap();
return (
<div className="w-[400px] h-[420px]">
<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')}
@@ -32,11 +32,14 @@ const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => {
</div>
)}
{!loading && (
<div className="bg-bg-card rounded-lg p-4 w-[400px] h-[380px]">
<div className="bg-bg-card rounded-lg p-4 w-full h-full">
<IndentedTree
data={data}
show
style={{ width: '100%', height: '100%' }}
style={{
width: '100%',
height: '100%',
}}
></IndentedTree>
</div>
)}

+ 0
- 37
web/src/pages/next-search/search-home.tsx Целия файл

@@ -1,5 +1,4 @@
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { cn } from '@/lib/utils';
import { Search } from 'lucide-react';
@@ -68,42 +67,6 @@ export default function SearchPage({
</div>
</div>
</div>

<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">Related Search</p>
<div className="mt-2 flex flex-wrap justify-start gap-2">
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search Related SearchRelated Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search Related SearchRelated Search
</Button>
<Button
variant="transparent"
className="bg-bg-card text-text-secondary"
>
Related Search
</Button>
</div>
</div>
</div>
</section>
);

+ 118
- 69
web/src/pages/next-search/search-setting.tsx Целия файл

@@ -12,7 +12,6 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Label } from '@/components/ui/label';
import {
MultiSelect,
MultiSelectOptionType,
@@ -42,6 +41,7 @@ import {
import {
ISearchAppDetailProps,
IUpdateSearchProps,
IllmSettingProps,
useUpdateSearch,
} from '../next-searches/hooks';
import {
@@ -55,14 +55,6 @@ interface SearchSettingProps {
className?: string;
data: ISearchAppDetailProps;
}
interface ISubmitLlmSettingProps {
llm_id: string;
parameter: string;
temperature?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}

const SearchSettingFormSchema = z
.object({
@@ -120,16 +112,19 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]);
const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState('');
const descriptionDefaultValue = 'You are an intelligent assistant.';
const resetForm = useCallback(() => {
formMethods.reset({
search_id: data?.id,
name: data?.name || '',
avatar: data?.avatar || '',
description: data?.description || 'You are an intelligent assistant.',
description: data?.description || descriptionDefaultValue,
search_config: {
kb_ids: search_config?.kb_ids || [],
vector_similarity_weight: search_config?.vector_similarity_weight || 20,
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: 0.0,
@@ -198,8 +193,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
})();
}
}, [avatarFile]);
const { list: datasetListOrigin, loading: datasetLoading } =
useFetchKnowledgeList();
const { list: datasetListOrigin } = useFetchKnowledgeList();

useEffect(() => {
const datasetListMap = datasetListOrigin.map((item: IKnowledge) => {
@@ -259,7 +253,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
) => {
try {
const { search_config, ...other_formdata } = formData;
const { llm_setting, ...other_config } = search_config;
const { llm_setting, vector_similarity_weight, ...other_config } =
search_config;
const llmSetting = {
llm_id: llm_setting.llm_id,
parameter: llm_setting.parameter,
@@ -267,7 +262,8 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
top_p: llm_setting.top_p,
frequency_penalty: llm_setting.frequency_penalty,
presence_penalty: llm_setting.presence_penalty,
} as ISubmitLlmSettingProps;
} as IllmSettingProps;

if (!llm_setting.frequencyPenaltyEnabled) {
delete llmSetting.frequency_penalty;
}
@@ -284,6 +280,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
...other_formdata,
search_config: {
...other_config,
vector_similarity_weight: 1 - vector_similarity_weight,
llm_setting: { ...llmSetting },
},
tenant_id: systemSetting.tenant_id,
@@ -355,46 +352,54 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormItem>
<FormLabel>Avatar</FormLabel>
<FormControl>
<div className="relative group">
{!avatarBase64Str ? (
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
<div className="flex flex-col items-center">
<Upload />
<p>{t('common.upload')}</p>
<div className="relative group flex items-end gap-2">
<div>
{!avatarBase64Str ? (
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
<div className="flex flex-col items-center">
<Upload />
<p>{t('common.upload')}</p>
</div>
</div>
</div>
) : (
<div className="w-[64px] h-[64px] relative grid place-content-center">
<RAGFlowAvatar
avatar={avatarBase64Str}
name={data.name}
className="w-[64px] h-[64px] rounded-md block"
/>
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
<Pencil
size={20}
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
) : (
<div className="w-[64px] h-[64px] relative grid place-content-center">
<RAGFlowAvatar
avatar={avatarBase64Str}
name={data.name}
className="w-[64px] h-[64px] rounded-md block"
/>
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
<Pencil
size={20}
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
/>
</div>
</div>
</div>
)}
<input
placeholder=""
// {...field}
type="file"
title=""
accept="image/*"
className="absolute w-[64px] top-0 left-0 h-full opacity-0 cursor-pointer"
onChange={(ev) => {
const file = ev.target?.files?.[0];
if (
/\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '')
) {
setAvatarFile(file!);
}
ev.target.value = '';
}}
/>
)}
<input
placeholder=""
// {...field}
type="file"
title=""
accept="image/*"
className="absolute w-[64px] top-0 left-0 h-full opacity-0 cursor-pointer"
onChange={(ev) => {
const file = ev.target?.files?.[0];
if (
/\.(jpg|jpeg|png|webp|bmp)$/i.test(
file?.name ?? '',
)
) {
setAvatarFile(file!);
}
ev.target.value = '';
}}
/>
</div>

<div className="margin-1 text-muted-foreground">
{t('knowledgeConfiguration.photoTip')}
</div>
</div>
</FormControl>
<FormMessage />
@@ -410,7 +415,20 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Description" {...field} />
<Input
placeholder="You are an intelligent assistant."
{...field}
onFocus={() => {
if (field.value === descriptionDefaultValue) {
field.onChange('');
}
}}
onBlur={() => {
if (field.value === '') {
field.onChange(descriptionDefaultValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -451,26 +469,58 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
control={formMethods.control}
name="search_config.vector_similarity_weight"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormItem>
<FormLabel>
<span className="text-destructive mr-1"> *</span>Keyword
Similarity Weight
</FormLabel>
<FormControl>
<div className="flex justify-between items-center">
<div
className={cn(
'flex items-center gap-4 justify-between',
className,
)}
>
<FormControl>
<SingleFormSlider
max={100}
step={1}
value={field.value as number}
onChange={(values) => field.onChange(values)}
{...field}
max={1}
min={0}
step={0.01}
></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={1}
min={0}
step={0.01}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
// <FormItem className="flex flex-col">
// <FormLabel>
// <span className="text-destructive mr-1"> *</span>Keyword
// Similarity Weight
// </FormLabel>
// <FormControl>
// {/* <div className="flex justify-between items-center">
// <SingleFormSlider
// max={100}
// step={1}
// value={field.value as number}
// onChange={(values) => field.onChange(values)}
// ></SingleFormSlider>
// <Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20">
// {field.value}
// </Label>
// </div> */}
// </FormControl>
// <FormMessage />
// </FormItem>
)}
/>

@@ -528,16 +578,15 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
<FormControl>
<SingleFormSlider
{...field}
max={100}
max={2048}
min={0}
step={1}
></SingleFormSlider>
</FormControl>
<FormControl>
<Input
type={'number'}
className="h-7 w-20 bg-bg-card"
max={100}
max={2048}
min={0}
step={1}
{...field}

+ 321
- 0
web/src/pages/next-search/search-view.tsx Целия файл

@@ -0,0 +1,321 @@
import { FileIcon } from '@/components/icon-font';
import { ImageWithPopover } from '@/components/image';
import { Input } from '@/components/originui/input';
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 { IReference } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import DOMPurify from 'dompurify';
import { TFunction } from 'i18next';
import { isEmpty } from 'lodash';
import { BrainCircuit, Search, Square, 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,
loading,
isFirstRender,
selectedDocumentIds,
isSearchStrEmpty,
searchStr,
stopOutputMessage,
visible,
hideModal,
documentId,
selectedChunk,
clickDocumentButton,
mindMapVisible,
hideMindMapModal,
showMindMapModal,
mindMapLoading,
mindMap,
chunks,
total,
handleSearch,
pagination,
onChange,
t,
}: ISearchReturnProps & {
setIsSearching?: Dispatch<SetStateAction<boolean>>;
searchData: ISearchAppDetailProps;
t: TFunction<'translation', undefined>;
}) {
const { t: tt, i18n } = 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-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text cursor-pointer',
)}
onClick={() => {
setIsSearching?.(false);
}}
>
RAGFlow
</h1>

<div
className={cn(
' rounded-lg text-primary text-xl sticky flex 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={tt('search.searchGreeting')}
className={cn(
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background',
)}
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"
size={14}
onClick={() => {
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary">|</span>
<button
type="button"
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
onClick={() => {
if (sendingLoading) {
stopOutputMessage();
} else {
handleSearch(searchtext);
}
}}
>
{sendingLoading ? (
<Square size={22} className="m-auto" />
) : (
<Search size={22} className="m-auto" />
)}
</button>
</div>
</div>
</div>
{/* search body */}
<div
className="w-full mt-5 overflow-auto scrollbar-none "
style={{ height: 'calc(100vh - 250px)' }}
>
{searchData.search_config.summary && !isSearchStrEmpty && (
<>
<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 */}
{!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 ">
<Spin spinning={loading}>
{chunks?.length > 0 && (
<>
{chunks.map((chunk, index) => {
return (
<>
<div
key={chunk.chunk_id}
className="w-full flex flex-col"
>
<div className="w-full">
<ImageWithPopover
id={chunk.img_id}
></ImageWithPopover>
<Popover>
<PopoverTrigger asChild>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
`${chunk.highlight}...`,
),
}}
className="text-sm text-text-primary mb-1"
></div>
</PopoverTrigger>
<PopoverContent className="text-text-primary">
<HightLightMarkdown>
{chunk.content_with_weight}
</HightLightMarkdown>
</PopoverContent>
</Popover>
</div>
<div
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
onClick={() =>
clickDocumentButton(chunk.doc_id, chunk as any)
}
>
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
</div>
</div>
{index < chunks.length - 1 && (
<div className="w-full border-b border-border-default/80 mt-6"></div>
)}
</>
);
})}
</>
)}
</Spin>
{relatedQuestions?.length > 0 && (
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">
Related Search
</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)}
>
Related Search{x}
</Button>
))}
</div>
</div>
)}
</div>
</div>

{total > 0 && (
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={total}
onChange={onChange}
></RAGFlowPagination>
</div>
)}
</div>

{mindMapVisible && (
<div className="flex-1 h-[88dvh] z-30 ml-8 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>
);
}

+ 18
- 317
web/src/pages/next-search/searching.tsx Целия файл

@@ -1,332 +1,33 @@
import { FileIcon } from '@/components/icon-font';
import { ImageWithPopover } from '@/components/image';
import { Input } from '@/components/originui/input';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Skeleton } from '@/components/ui/skeleton';
import { Spin } from '@/components/ui/spin';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { IReference } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import DOMPurify from 'dompurify';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { BrainCircuit, Search, Square, Tag, X } from 'lucide-react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
} from 'react';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { ISearchAppDetailProps } from '../next-searches/hooks';
import { useSendQuestion, useShowMindMapDrawer } from '../search/hooks';
import PdfDrawer from './document-preview-modal';
import HightLightMarkdown from './highlight-markdown';
import { useSearching } from './hooks';
import './index.less';
import styles from './index.less';
import MarkdownContent from './markdown-content';
import MindMapDrawer from './mindmap-drawer';
import RetrievalDocuments from './retrieval-documents';
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 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);
};

const searchingParam = useSearching({
searchText,
data: searchData,
setIsSearching,
setSearchText,
});
const { t } = useTranslation();
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-r from-sky-600 from-30% via-sky-500 via-60% to-emerald-500 bg-clip-text cursor-pointer',
)}
onClick={() => {
setIsSearching(false);
}}
>
RAGFlow
</h1>

<div
className={cn(
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
)}
>
<div className={cn('flex flex-col justify-start items-start w-full')}>
<div className="relative w-full text-primary">
<Input
ref={inputRef}
key="search-input"
placeholder="How can I help you today?"
className={cn(
'w-full rounded-full py-6 pl-4 !pr-[8rem] text-primary text-lg bg-background',
)}
value={searchStr}
onChange={handleSearchStrChange}
disabled={sendingLoading}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
<X
className="text-text-secondary"
size={14}
onClick={() => {
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary">|</span>
<button
type="button"
className="rounded-full bg-white p-1 text-gray-800 shadow w-12 h-8 ml-4"
onClick={() => {
if (sendingLoading) {
stopOutputMessage();
} else {
handleSearch();
}
}}
>
{sendingLoading ? (
<Square size={22} className="m-auto" />
) : (
<Search size={22} className="m-auto" />
)}
</button>
</div>
</div>
</div>

{/* search body */}
<div
className="w-full mt-5 overflow-auto scrollbar-none "
style={{ height: 'calc(100vh - 250px)' }}
>
{searchData.search_config.summary && (
<>
<div className="flex justify-start items-start text-text-primary text-2xl">
AI Summary
</div>
{isEmpty(answer) && sendingLoading ? (
<div className="space-y-2">
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-2/3 bg-bg-card" />
</div>
) : (
answer.answer && (
<div className="border rounded-lg p-4 mt-3 max-h-52 overflow-auto scrollbar-none">
<MarkdownContent
loading={sendingLoading}
content={answer.answer}
reference={answer.reference ?? ({} as IReference)}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
)
)}
</>
)}

<div className="w-full border-b border-border-default/80 my-6"></div>
{/* retrieval documents */}
<div className=" mt-3 w-44 ">
<RetrievalDocuments
selectedDocumentIds={selectedDocumentIds}
setSelectedDocumentIds={setSelectedDocumentIds}
onTesting={handleTestChunk}
></RetrievalDocuments>
</div>
<div className="w-full border-b border-border-default/80 my-6"></div>
<div className="mt-3 ">
<Spin spinning={loading}>
{chunks?.length > 0 && (
<>
{chunks.map((chunk, index) => {
return (
<>
<div
key={chunk.chunk_id}
className="w-full flex flex-col"
>
<div className="w-full">
<ImageWithPopover
id={chunk.img_id}
></ImageWithPopover>
<Popover>
<PopoverTrigger asChild>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
`${chunk.highlight}...`,
),
}}
className="text-sm text-text-primary mb-1"
></div>
</PopoverTrigger>
<PopoverContent className="text-text-primary">
<HightLightMarkdown>
{chunk.content_with_weight}
</HightLightMarkdown>
</PopoverContent>
</Popover>
</div>
<div
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
onClick={() =>
clickDocumentButton(chunk.doc_id, chunk as any)
}
>
<FileIcon name={chunk.docnm_kwd}></FileIcon>
{chunk.docnm_kwd}
</div>
</div>
{index < chunks.length - 1 && (
<div className="w-full border-b border-border-default/80 mt-6"></div>
)}
</>
);
})}
</>
)}
</Spin>
{relatedQuestions?.length > 0 && (
<div title={t('chat.relatedQuestion')}>
<div className="flex gap-2">
{relatedQuestions?.map((x, idx) => (
<Tag
key={idx}
className={styles.tag}
onClick={handleClickRelatedQuestion(x)}
>
{x}
</Tag>
))}
</div>
</div>
)}
</div>
</div>
<div className="mt-8 px-8 pb-8">
<RAGFlowPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={total}
onChange={onChange}
></RAGFlowPagination>
</div>
</div>
</div>
{!mindMapVisible &&
!isFirstRender &&
!isSearchStrEmpty &&
!isEmpty(searchData.search_config.kb_ids) && (
<Popover>
<PopoverTrigger asChild>
<Button
className="rounded-lg h-8 w-8 p-0 absolute top-28 right-3 z-30"
variant={'transparent'}
onClick={showMindMapModal}
>
{/* <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> */}
<BrainCircuit size={24} />
</Button>
</PopoverTrigger>
<PopoverContent className="w-fit">{t('chunk.mind')}</PopoverContent>
</Popover>
)}
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
{mindMapVisible && (
<div className="absolute top-20 right-16 z-30">
<MindMapDrawer
visible={mindMapVisible}
hideModal={hideMindMapModal}
data={mindMap}
loading={mindMapLoading}
></MindMapDrawer>
</div>
)}
</section>
<SearchingView
{...searchingParam}
searchData={searchData}
setIsSearching={setIsSearching}
t={t}
/>
);
}

+ 35
- 0
web/src/pages/next-search/share/index.tsx Целия файл

@@ -0,0 +1,35 @@
import i18n from '@/locales/config';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
ISearchAppDetailProps,
useFetchSearchDetail,
} from '../../next-searches/hooks';
import { useGetSharedSearchParams, useSearching } from '../hooks';
import '../index.less';
import SearchingView from '../search-view';
export default function SearchingPage() {
const { tenantId, locale } = useGetSharedSearchParams();
const {
data: searchData = {
search_config: { kb_ids: [] },
} as unknown as ISearchAppDetailProps,
} = useFetchSearchDetail(tenantId as string);
const searchingParam = useSearching({
data: searchData,
});
const { t } = useTranslation();

// useEffect(() => {
// if (locale) {
// i18n.changeLanguage(locale);
// }
// }, [locale, i18n]);
useEffect(() => {
console.log('locale', locale, i18n.language);
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale]);
return <SearchingView {...searchingParam} searchData={searchData} t={t} />;
}

+ 26
- 11
web/src/pages/next-searches/hooks.ts Целия файл

@@ -5,7 +5,7 @@ import searchService from '@/services/search-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useParams, useSearchParams } from 'umi';

interface CreateSearchProps {
name: string;
@@ -156,13 +156,13 @@ export const useDeleteSearch = () => {
return { data, isError, deleteSearch };
};

interface IllmSettingProps {
export interface IllmSettingProps {
llm_id: string;
parameter: string;
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
temperature?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
interface IllmSettingEnableProps {
temperatureEnabled?: boolean;
@@ -204,14 +204,29 @@ interface SearchDetailResponse {
message: string;
}

export const useFetchSearchDetail = () => {
export const useFetchSearchDetail = (tenantId?: string) => {
const { id } = useParams();

const [searchParams] = useSearchParams();
const shared_id = searchParams.get('shared_id');
const searchId = id || shared_id;
let param: { search_id: string | null; tenant_id?: string } = {
search_id: searchId,
};
if (shared_id) {
param = {
search_id: searchId,
tenant_id: tenantId,
};
}
const fetchSearchDetailFunc = shared_id
? searchService.getSearchDetailShare
: searchService.getSearchDetail;
const { data, isLoading, isError } = useQuery<SearchDetailResponse, Error>({
queryKey: ['searchDetail', id],
queryKey: ['searchDetail', searchId],
enabled: !shared_id || !!tenantId,
queryFn: async () => {
const { data: response } = await searchService.getSearchDetail({
search_id: id,
});
const { data: response } = await fetchSearchDetailFunc(param);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to fetch search detail');
}

+ 16
- 11
web/src/pages/next-searches/index.tsx Целия файл

@@ -66,7 +66,7 @@ export default function SearchList() {
setSearchListParams({ ...searchParams, page, page_size: pageSize });
};
return (
<section>
<section className="w-full h-full flex flex-col">
<div className="px-8 pt-8">
<ListFilterBar
icon={
@@ -89,18 +89,23 @@ export default function SearchList() {
</Button>
</ListFilterBar>
</div>
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8">
{list?.data.search_apps.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})}
<div className="flex-1">
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8">
{list?.data.search_apps.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})}
</div>
</div>
{list?.data.total && (
<RAGFlowPagination
{...pick(searchParams, 'current', 'pageSize')}
total={list?.data.total}
onChange={handlePageChange}
/>
{list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
{...pick(searchParams, 'current', 'pageSize')}
total={list?.data.total}
onChange={handlePageChange}
/>
</div>
)}

<Modal
open={openCreateModal}
onOpenChange={(open) => {

+ 4
- 13
web/src/pages/next-searches/search-card.tsx Целия файл

@@ -19,7 +19,7 @@ export function SearchCard({ data }: IProps) {
navigateToSearch(data?.id);
}}
>
<CardContent className="p-4 flex gap-2 items-start group">
<CardContent className="p-4 flex gap-2 items-start group h-full">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"
@@ -27,7 +27,7 @@ export function SearchCard({ data }: IProps) {
name={data.name}
/>
</div>
<div className="flex flex-col gap-1 flex-1">
<div className="flex flex-col justify-between gap-1 flex-1 h-full">
<section className="flex justify-between">
<div className="text-[20px] font-bold w-80% leading-5">
{data.name}
@@ -37,22 +37,13 @@ export function SearchCard({ data }: IProps) {
</SearchDropdown>
</section>

<div>{data.description}</div>
<section className="flex justify-between">
<section className="flex flex-col gap-1 mt-1">
<div>{data.description}</div>
<div>
Search app
<p className="text-sm opacity-80">
{formatDate(data.update_time)}
</p>
</div>
{/* <div className="space-x-2 invisible group-hover:visible">
<Button variant="icon" size="icon" onClick={navigateToSearch}>
<ChevronRight className="h-6 w-6" />
</Button>
<Button variant="icon" size="icon">
<Trash2 />
</Button>
</div> */}
</section>
</div>
</CardContent>

+ 15
- 8
web/src/pages/search/hooks.ts Целия файл

@@ -1,4 +1,4 @@
import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks';
import { useFetchRelatedQuestions } from '@/hooks/chat-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import {
useTestChunkAllRetrieval,
@@ -18,17 +18,23 @@ import {
useRef,
useState,
} from 'react';
import {
useGetSharedSearchParams,
useSearchFetchMindMap,
} from '../next-search/hooks';

export const useSendQuestion = (kbIds: string[]) => {
export const useSendQuestion = (kbIds: string[], tenantId?: string) => {
const { sharedId } = useGetSharedSearchParams();
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
api.ask,
sharedId ? api.askShare : api.ask,
);
const { testChunk, loading } = useTestChunkRetrieval();
const { testChunkAll } = useTestChunkAllRetrieval();

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();
useFetchRelatedQuestions(tenantId);
const [searchStr, setSearchStr] = useState<string>('');
const [isFirstRender, setIsFirstRender] = useState(true);
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
@@ -43,7 +49,7 @@ export const useSendQuestion = (kbIds: string[]) => {
setIsFirstRender(false);
setCurrentAnswer({} as IAnswer);
setSendingLoading(true);
send({ kb_ids: kbIds, question: q });
send({ kb_ids: kbIds, question: q, tenantId });
testChunk({
kb_id: kbIds,
highlight: true,
@@ -61,6 +67,7 @@ export const useSendQuestion = (kbIds: string[]) => {
fetchRelatedQuestions,
setPagination,
pagination.pageSize,
tenantId,
],
);

@@ -218,7 +225,7 @@ export const useShowMindMapDrawer = (kbIds: string[], question: string) => {
fetchMindMap,
data: mindMap,
loading: mindMapLoading,
} = useFetchMindMap();
} = useSearchFetchMindMap();

const handleShowModal = useCallback(() => {
const searchParams = { question: trim(question), kb_ids: kbIds };

+ 6
- 0
web/src/routes.ts Целия файл

@@ -11,6 +11,7 @@ export enum Routes {
AgentList = '/agent-list',
Searches = '/next-searches',
Search = '/next-search',
SearchShare = '/next-search/share',
Chats = '/next-chats',
Chat = '/next-chat',
Files = '/files',
@@ -234,6 +235,11 @@ const routes = [
layout: false,
component: `@/pages${Routes.Search}`,
},
{
path: `${Routes.SearchShare}`,
layout: false,
component: `@/pages${Routes.SearchShare}`,
},
{
path: Routes.Agents,
layout: false,

+ 5
- 0
web/src/services/knowledge-service.ts Целия файл

@@ -38,6 +38,7 @@ const {
listTagByKnowledgeIds,
setMeta,
getMeta,
retrievalTestShare,
} = api;

const methods = {
@@ -164,6 +165,10 @@ const methods = {
url: getMeta,
method: 'get',
},
retrievalTestShare: {
url: retrievalTestShare,
method: 'post',
},
};

const kbService = registerServer<keyof typeof methods>(methods, request);

+ 21
- 0
web/src/services/search-service.ts Целия файл

@@ -8,6 +8,10 @@ const {
deleteSearch,
getSearchDetail,
updateSearchSetting,
askShare,
mindmapShare,
getRelatedQuestionsShare,
getSearchDetailShare,
} = api;
const methods = {
createSearch: {
@@ -27,6 +31,23 @@ const methods = {
url: updateSearchSetting,
method: 'post',
},
askShare: {
url: askShare,
method: 'post',
},
mindmapShare: {
url: mindmapShare,
method: 'post',
},
getRelatedQuestionsShare: {
url: getRelatedQuestionsShare,
method: 'post',
},

getSearchDetailShare: {
url: getSearchDetailShare,
method: 'get',
},
} as const;
const searchService = registerServer<keyof typeof methods>(methods, request);


+ 5
- 0
web/src/utils/api.ts Целия файл

@@ -181,5 +181,10 @@ export default {
getSearchList: `${api_host}/search/list`,
deleteSearch: `${api_host}/search/rm`,
getSearchDetail: `${api_host}/search/detail`,
getSearchDetailShare: `${ExternalApi}${api_host}/searchbots/detail`,
updateSearchSetting: `${api_host}/search/update`,
askShare: `${ExternalApi}${api_host}/searchbots/ask`,
mindmapShare: `${ExternalApi}${api_host}/searchbots/mindmap`,
getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`,
retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`,
};

Loading…
Отказ
Запис