### 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
| @@ -22,6 +22,7 @@ export const variableEnabledFieldMap = { | |||
| export enum SharedFrom { | |||
| Agent = 'agent', | |||
| Chat = 'chat', | |||
| Search = 'search', | |||
| } | |||
| export enum ChatSearchParams { | |||
| @@ -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) { | |||
| @@ -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 ?', | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -1185,9 +1185,13 @@ export default { | |||
| knowledge: '知識', | |||
| chat: '聊天', | |||
| }, | |||
| }, | |||
| modal: { | |||
| okText: '確認', | |||
| cancelText: '取消', | |||
| modal: { | |||
| okText: '確認', | |||
| cancelText: '取消', | |||
| }, | |||
| search: { | |||
| createSearch: '新建查詢', | |||
| searchGreeting: '今天我能為你做些什麽?', | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -1316,12 +1316,13 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| }, | |||
| }, | |||
| }, | |||
| }, | |||
| modal: { | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| }, | |||
| search: { | |||
| createSearch: '新建查询', | |||
| modal: { | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| }, | |||
| search: { | |||
| createSearch: '新建查询', | |||
| searchGreeting: '今天我能为你做些什么?', | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -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(); | |||
| @@ -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} | |||
| @@ -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; | |||
| @@ -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, | |||
| }; | |||
| }; | |||
| @@ -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 | |||
| @@ -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> | |||
| )} | |||
| @@ -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> | |||
| ); | |||
| @@ -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} | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -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} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -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} />; | |||
| } | |||
| @@ -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'); | |||
| } | |||
| @@ -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) => { | |||
| @@ -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> | |||
| @@ -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 }; | |||
| @@ -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, | |||
| @@ -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); | |||
| @@ -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); | |||
| @@ -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`, | |||
| }; | |||