### 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
| export enum SharedFrom { | export enum SharedFrom { | ||||
| Agent = 'agent', | Agent = 'agent', | ||||
| Chat = 'chat', | Chat = 'chat', | ||||
| Search = 'search', | |||||
| } | } | ||||
| export enum ChatSearchParams { | export enum ChatSearchParams { | 
| import { get, set } from 'lodash'; | import { get, set } from 'lodash'; | ||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useParams } from 'umi'; | |||||
| import { useParams, useSearchParams } from 'umi'; | |||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||
| import { | import { | ||||
| useGetPaginationWithRouter, | useGetPaginationWithRouter, | ||||
| // Only one file can be uploaded at a time | // Only one file can be uploaded at a time | ||||
| export const useUploadCanvasFile = () => { | export const useUploadCanvasFile = () => { | ||||
| const { id } = useParams(); | const { id } = useParams(); | ||||
| const [searchParams] = useSearchParams(); | |||||
| const shared_id = searchParams.get('shared_id'); | |||||
| const canvasId = id || shared_id; | |||||
| const { | const { | ||||
| data, | data, | ||||
| isPending: loading, | isPending: loading, | ||||
| } | } | ||||
| const { data } = await agentService.uploadCanvasFile( | const { data } = await agentService.uploadCanvasFile( | ||||
| { url: api.uploadAgentFile(id), data: nextBody }, | |||||
| { url: api.uploadAgentFile(canvasId as string), data: nextBody }, | |||||
| true, | true, | ||||
| ); | ); | ||||
| if (data?.code === 0) { | if (data?.code === 0) { | 
| }, | }, | ||||
| search: { | search: { | ||||
| createSearch: 'Create Search', | createSearch: 'Create Search', | ||||
| searchGreeting: 'How can I help you today ?', | |||||
| }, | }, | ||||
| }, | }, | ||||
| }; | }; | 
| knowledge: '知識', | knowledge: '知識', | ||||
| chat: '聊天', | chat: '聊天', | ||||
| }, | }, | ||||
| }, | |||||
| modal: { | |||||
| okText: '確認', | |||||
| cancelText: '取消', | |||||
| modal: { | |||||
| okText: '確認', | |||||
| cancelText: '取消', | |||||
| }, | |||||
| search: { | |||||
| createSearch: '新建查詢', | |||||
| searchGreeting: '今天我能為你做些什麽?', | |||||
| }, | |||||
| }, | }, | ||||
| }; | }; | 
| }, | }, | ||||
| }, | }, | ||||
| }, | }, | ||||
| }, | |||||
| modal: { | |||||
| okText: '确认', | |||||
| cancelText: '取消', | |||||
| }, | |||||
| search: { | |||||
| createSearch: '新建查询', | |||||
| modal: { | |||||
| okText: '确认', | |||||
| cancelText: '取消', | |||||
| }, | |||||
| search: { | |||||
| createSearch: '新建查询', | |||||
| searchGreeting: '今天我能为你做些什么?', | |||||
| }, | |||||
| }, | }, | ||||
| }; | }; | 
| return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`; | return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`; | ||||
| }; | }; | ||||
| const useFetchTokenListBeforeOtherStep = () => { | |||||
| export const useFetchTokenListBeforeOtherStep = () => { | |||||
| const { showTokenEmptyError } = useShowTokenEmptyError(); | const { showTokenEmptyError } = useShowTokenEmptyError(); | ||||
| const { showBetaEmptyError } = useShowBetaEmptyError(); | const { showBetaEmptyError } = useShowBetaEmptyError(); | ||||
| interface IProps extends IModalProps<any> { | interface IProps extends IModalProps<any> { | ||||
| documentId: string; | documentId: string; | ||||
| chunk: IChunk | IReferenceChunk; | |||||
| chunk: IChunk & | |||||
| IReferenceChunk & { docnm_kwd: string; document_name: string }; | |||||
| } | } | ||||
| function getFileExtensionRegex(filename: string): string { | function getFileExtensionRegex(filename: string): string { | ||||
| const match = filename.match(/\.([^.]+)$/); | const match = filename.match(/\.([^.]+)$/); | ||||
| // const [loaded, setLoaded] = useState(false); | // const [loaded, setLoaded] = useState(false); | ||||
| const url = getDocumentUrl(); | const url = getDocumentUrl(); | ||||
| console.log('chunk--->', chunk.docnm_kwd, url); | |||||
| const [fileType, setFileType] = useState(''); | const [fileType, setFileType] = useState(''); | ||||
| useEffect(() => { | 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); | setFileType(type); | ||||
| } | } | ||||
| }, [chunk.docnm_kwd]); | |||||
| }, [chunk.docnm_kwd, chunk.document_name]); | |||||
| return ( | return ( | ||||
| <Modal | <Modal | ||||
| title={ | title={ | ||||
| <div className="flex items-center gap-2"> | <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> | </div> | ||||
| } | } | ||||
| onCancel={hideModal} | onCancel={hideModal} | 
| 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; | 
| 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, | |||||
| }; | |||||
| }; | 
| BreadcrumbSeparator, | BreadcrumbSeparator, | ||||
| } from '@/components/ui/breadcrumb'; | } from '@/components/ui/breadcrumb'; | ||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { SharedFrom } from '@/constants/chat'; | |||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | 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 { useEffect, useState } from 'react'; | ||||
| import { useFetchTokenListBeforeOtherStep } from '../agent/hooks/use-show-dialog'; | |||||
| import { | import { | ||||
| ISearchAppDetailProps, | ISearchAppDetailProps, | ||||
| useFetchSearchDetail, | useFetchSearchDetail, | ||||
| } from '../next-searches/hooks'; | } from '../next-searches/hooks'; | ||||
| import EmbedAppModal from './embed-app-modal'; | |||||
| import './index.less'; | import './index.less'; | ||||
| import SearchHome from './search-home'; | import SearchHome from './search-home'; | ||||
| import { SearchSetting } from './search-setting'; | import { SearchSetting } from './search-setting'; | ||||
| const { navigateToSearchList } = useNavigatePage(); | const { navigateToSearchList } = useNavigatePage(); | ||||
| const [isSearching, setIsSearching] = useState(false); | const [isSearching, setIsSearching] = useState(false); | ||||
| const { data: SearchData } = useFetchSearchDetail(); | const { data: SearchData } = useFetchSearchDetail(); | ||||
| const { beta, handleOperate } = useFetchTokenListBeforeOtherStep(); | |||||
| const [openSetting, setOpenSetting] = useState(false); | const [openSetting, setOpenSetting] = useState(false); | ||||
| const [openEmbed, setOpenEmbed] = useState(false); | |||||
| const [searchText, setSearchText] = useState(''); | const [searchText, setSearchText] = useState(''); | ||||
| const { data: tenantInfo } = useFetchTenantInfo(); | |||||
| const tenantId = tenantInfo.tenant_id; | |||||
| useEffect(() => { | |||||
| handleOperate(); | |||||
| }, [handleOperate]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (isSearching) { | if (isSearching) { | ||||
| setOpenSetting(false); | setOpenSetting(false); | ||||
| data={SearchData as ISearchAppDetailProps} | 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> | </div> | ||||
| {!isSearching && ( | {!isSearching && ( | ||||
| <div className="absolute left-5 bottom-12 "> | <div className="absolute left-5 bottom-12 "> | ||||
| <Button | <Button | 
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const percent = usePendingMindMap(); | const percent = usePendingMindMap(); | ||||
| return ( | 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="flex w-full justify-between items-center mb-2"> | ||||
| <div className="text-text-primary font-medium text-base"> | <div className="text-text-primary font-medium text-base"> | ||||
| {t('chunk.mind')} | {t('chunk.mind')} | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {!loading && ( | {!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 | <IndentedTree | ||||
| data={data} | data={data} | ||||
| show | show | ||||
| style={{ width: '100%', height: '100%' }} | |||||
| style={{ | |||||
| width: '100%', | |||||
| height: '100%', | |||||
| }} | |||||
| ></IndentedTree> | ></IndentedTree> | ||||
| </div> | </div> | ||||
| )} | )} | 
| import { Input } from '@/components/originui/input'; | import { Input } from '@/components/originui/input'; | ||||
| import { Button } from '@/components/ui/button'; | |||||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { Search } from 'lucide-react'; | import { Search } from 'lucide-react'; | ||||
| </div> | </div> | ||||
| </div> | </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> | </div> | ||||
| </section> | </section> | ||||
| ); | ); | 
| FormLabel, | FormLabel, | ||||
| FormMessage, | FormMessage, | ||||
| } from '@/components/ui/form'; | } from '@/components/ui/form'; | ||||
| import { Label } from '@/components/ui/label'; | |||||
| import { | import { | ||||
| MultiSelect, | MultiSelect, | ||||
| MultiSelectOptionType, | MultiSelectOptionType, | ||||
| import { | import { | ||||
| ISearchAppDetailProps, | ISearchAppDetailProps, | ||||
| IUpdateSearchProps, | IUpdateSearchProps, | ||||
| IllmSettingProps, | |||||
| useUpdateSearch, | useUpdateSearch, | ||||
| } from '../next-searches/hooks'; | } from '../next-searches/hooks'; | ||||
| import { | import { | ||||
| className?: string; | className?: string; | ||||
| data: ISearchAppDetailProps; | data: ISearchAppDetailProps; | ||||
| } | } | ||||
| interface ISubmitLlmSettingProps { | |||||
| llm_id: string; | |||||
| parameter: string; | |||||
| temperature?: number; | |||||
| top_p?: number; | |||||
| frequency_penalty?: number; | |||||
| presence_penalty?: number; | |||||
| } | |||||
| const SearchSettingFormSchema = z | const SearchSettingFormSchema = z | ||||
| .object({ | .object({ | ||||
| const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 | const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 | ||||
| const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]); | const [datasetList, setDatasetList] = useState<MultiSelectOptionType[]>([]); | ||||
| const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState(''); | const [datasetSelectEmbdId, setDatasetSelectEmbdId] = useState(''); | ||||
| const descriptionDefaultValue = 'You are an intelligent assistant.'; | |||||
| const resetForm = useCallback(() => { | const resetForm = useCallback(() => { | ||||
| formMethods.reset({ | formMethods.reset({ | ||||
| search_id: data?.id, | search_id: data?.id, | ||||
| name: data?.name || '', | name: data?.name || '', | ||||
| avatar: data?.avatar || '', | avatar: data?.avatar || '', | ||||
| description: data?.description || 'You are an intelligent assistant.', | |||||
| description: data?.description || descriptionDefaultValue, | |||||
| search_config: { | search_config: { | ||||
| kb_ids: search_config?.kb_ids || [], | 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, | web_search: search_config?.web_search || false, | ||||
| doc_ids: [], | doc_ids: [], | ||||
| similarity_threshold: 0.0, | similarity_threshold: 0.0, | ||||
| })(); | })(); | ||||
| } | } | ||||
| }, [avatarFile]); | }, [avatarFile]); | ||||
| const { list: datasetListOrigin, loading: datasetLoading } = | |||||
| useFetchKnowledgeList(); | |||||
| const { list: datasetListOrigin } = useFetchKnowledgeList(); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const datasetListMap = datasetListOrigin.map((item: IKnowledge) => { | const datasetListMap = datasetListOrigin.map((item: IKnowledge) => { | ||||
| ) => { | ) => { | ||||
| try { | try { | ||||
| const { search_config, ...other_formdata } = formData; | 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 = { | const llmSetting = { | ||||
| llm_id: llm_setting.llm_id, | llm_id: llm_setting.llm_id, | ||||
| parameter: llm_setting.parameter, | parameter: llm_setting.parameter, | ||||
| top_p: llm_setting.top_p, | top_p: llm_setting.top_p, | ||||
| frequency_penalty: llm_setting.frequency_penalty, | frequency_penalty: llm_setting.frequency_penalty, | ||||
| presence_penalty: llm_setting.presence_penalty, | presence_penalty: llm_setting.presence_penalty, | ||||
| } as ISubmitLlmSettingProps; | |||||
| } as IllmSettingProps; | |||||
| if (!llm_setting.frequencyPenaltyEnabled) { | if (!llm_setting.frequencyPenaltyEnabled) { | ||||
| delete llmSetting.frequency_penalty; | delete llmSetting.frequency_penalty; | ||||
| } | } | ||||
| ...other_formdata, | ...other_formdata, | ||||
| search_config: { | search_config: { | ||||
| ...other_config, | ...other_config, | ||||
| vector_similarity_weight: 1 - vector_similarity_weight, | |||||
| llm_setting: { ...llmSetting }, | llm_setting: { ...llmSetting }, | ||||
| }, | }, | ||||
| tenant_id: systemSetting.tenant_id, | tenant_id: systemSetting.tenant_id, | ||||
| <FormItem> | <FormItem> | ||||
| <FormLabel>Avatar</FormLabel> | <FormLabel>Avatar</FormLabel> | ||||
| <FormControl> | <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> | |||||
| ) : ( | |||||
| <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> | ||||
| </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> | </div> | ||||
| </FormControl> | </FormControl> | ||||
| <FormMessage /> | <FormMessage /> | ||||
| <FormItem> | <FormItem> | ||||
| <FormLabel>Description</FormLabel> | <FormLabel>Description</FormLabel> | ||||
| <FormControl> | <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> | </FormControl> | ||||
| <FormMessage /> | <FormMessage /> | ||||
| </FormItem> | </FormItem> | ||||
| control={formMethods.control} | control={formMethods.control} | ||||
| name="search_config.vector_similarity_weight" | name="search_config.vector_similarity_weight" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem className="flex flex-col"> | |||||
| <FormItem> | |||||
| <FormLabel> | <FormLabel> | ||||
| <span className="text-destructive mr-1"> *</span>Keyword | <span className="text-destructive mr-1"> *</span>Keyword | ||||
| Similarity Weight | Similarity Weight | ||||
| </FormLabel> | </FormLabel> | ||||
| <FormControl> | |||||
| <div className="flex justify-between items-center"> | |||||
| <div | |||||
| className={cn( | |||||
| 'flex items-center gap-4 justify-between', | |||||
| className, | |||||
| )} | |||||
| > | |||||
| <FormControl> | |||||
| <SingleFormSlider | <SingleFormSlider | ||||
| max={100} | |||||
| step={1} | |||||
| value={field.value as number} | |||||
| onChange={(values) => field.onChange(values)} | |||||
| {...field} | |||||
| max={1} | |||||
| min={0} | |||||
| step={0.01} | |||||
| ></SingleFormSlider> | ></SingleFormSlider> | ||||
| <Label className="w-10 h-6 bg-bg-card flex justify-center items-center rounded-lg ml-20"> | |||||
| {field.value} | |||||
| </Label> | |||||
| </div> | |||||
| </FormControl> | |||||
| </FormControl> | |||||
| <FormControl> | |||||
| <Input | |||||
| type={'number'} | |||||
| className="h-7 w-20 bg-bg-card" | |||||
| max={1} | |||||
| min={0} | |||||
| step={0.01} | |||||
| {...field} | |||||
| ></Input> | |||||
| </FormControl> | |||||
| </div> | |||||
| <FormMessage /> | <FormMessage /> | ||||
| </FormItem> | </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> | |||||
| )} | )} | ||||
| /> | /> | ||||
| <FormControl> | <FormControl> | ||||
| <SingleFormSlider | <SingleFormSlider | ||||
| {...field} | {...field} | ||||
| max={100} | |||||
| max={2048} | |||||
| min={0} | min={0} | ||||
| step={1} | step={1} | ||||
| ></SingleFormSlider> | ></SingleFormSlider> | ||||
| </FormControl> | </FormControl> | ||||
| <FormControl> | <FormControl> | ||||
| <Input | <Input | ||||
| type={'number'} | |||||
| className="h-7 w-20 bg-bg-card" | className="h-7 w-20 bg-bg-card" | ||||
| max={100} | |||||
| max={2048} | |||||
| min={0} | min={0} | ||||
| step={1} | step={1} | ||||
| {...field} | {...field} | 
| 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> | |||||
| ); | |||||
| } | 
| 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 { 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 './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({ | export default function SearchingPage({ | ||||
| searchText, | searchText, | ||||
| data: searchData, | data: searchData, | ||||
| setIsSearching, | setIsSearching, | ||||
| setSearchText, | |||||
| }: { | }: { | ||||
| searchText: string; | searchText: string; | ||||
| setIsSearching: Dispatch<SetStateAction<boolean>>; | setIsSearching: Dispatch<SetStateAction<boolean>>; | ||||
| setSearchText: Dispatch<SetStateAction<string>>; | setSearchText: Dispatch<SetStateAction<string>>; | ||||
| data: ISearchAppDetailProps; | 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 ( | 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} | |||||
| /> | |||||
| ); | ); | ||||
| } | } | 
| 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} />; | |||||
| } | 
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useParams } from 'umi'; | |||||
| import { useParams, useSearchParams } from 'umi'; | |||||
| interface CreateSearchProps { | interface CreateSearchProps { | ||||
| name: string; | name: string; | ||||
| return { data, isError, deleteSearch }; | return { data, isError, deleteSearch }; | ||||
| }; | }; | ||||
| interface IllmSettingProps { | |||||
| export interface IllmSettingProps { | |||||
| llm_id: string; | llm_id: string; | ||||
| parameter: 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 { | interface IllmSettingEnableProps { | ||||
| temperatureEnabled?: boolean; | temperatureEnabled?: boolean; | ||||
| message: string; | message: string; | ||||
| } | } | ||||
| export const useFetchSearchDetail = () => { | |||||
| export const useFetchSearchDetail = (tenantId?: string) => { | |||||
| const { id } = useParams(); | 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>({ | const { data, isLoading, isError } = useQuery<SearchDetailResponse, Error>({ | ||||
| queryKey: ['searchDetail', id], | |||||
| queryKey: ['searchDetail', searchId], | |||||
| enabled: !shared_id || !!tenantId, | |||||
| queryFn: async () => { | queryFn: async () => { | ||||
| const { data: response } = await searchService.getSearchDetail({ | |||||
| search_id: id, | |||||
| }); | |||||
| const { data: response } = await fetchSearchDetailFunc(param); | |||||
| if (response.code !== 0) { | if (response.code !== 0) { | ||||
| throw new Error(response.message || 'Failed to fetch search detail'); | throw new Error(response.message || 'Failed to fetch search detail'); | ||||
| } | } | 
| setSearchListParams({ ...searchParams, page, page_size: pageSize }); | setSearchListParams({ ...searchParams, page, page_size: pageSize }); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <section> | |||||
| <section className="w-full h-full flex flex-col"> | |||||
| <div className="px-8 pt-8"> | <div className="px-8 pt-8"> | ||||
| <ListFilterBar | <ListFilterBar | ||||
| icon={ | icon={ | ||||
| </Button> | </Button> | ||||
| </ListFilterBar> | </ListFilterBar> | ||||
| </div> | </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> | </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 | <Modal | ||||
| open={openCreateModal} | open={openCreateModal} | ||||
| onOpenChange={(open) => { | onOpenChange={(open) => { | 
| navigateToSearch(data?.id); | 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"> | <div className="flex justify-between mb-4"> | ||||
| <RAGFlowAvatar | <RAGFlowAvatar | ||||
| className="w-[32px] h-[32px]" | className="w-[32px] h-[32px]" | ||||
| name={data.name} | name={data.name} | ||||
| /> | /> | ||||
| </div> | </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"> | <section className="flex justify-between"> | ||||
| <div className="text-[20px] font-bold w-80% leading-5"> | <div className="text-[20px] font-bold w-80% leading-5"> | ||||
| {data.name} | {data.name} | ||||
| </SearchDropdown> | </SearchDropdown> | ||||
| </section> | </section> | ||||
| <div>{data.description}</div> | |||||
| <section className="flex justify-between"> | |||||
| <section className="flex flex-col gap-1 mt-1"> | |||||
| <div>{data.description}</div> | |||||
| <div> | <div> | ||||
| Search app | |||||
| <p className="text-sm opacity-80"> | <p className="text-sm opacity-80"> | ||||
| {formatDate(data.update_time)} | {formatDate(data.update_time)} | ||||
| </p> | </p> | ||||
| </div> | </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> | </section> | ||||
| </div> | </div> | ||||
| </CardContent> | </CardContent> | 
| import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks'; | |||||
| import { useFetchRelatedQuestions } from '@/hooks/chat-hooks'; | |||||
| import { useSetModalState } from '@/hooks/common-hooks'; | import { useSetModalState } from '@/hooks/common-hooks'; | ||||
| import { | import { | ||||
| useTestChunkAllRetrieval, | useTestChunkAllRetrieval, | ||||
| useRef, | useRef, | ||||
| useState, | useState, | ||||
| } from 'react'; | } 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( | 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 [sendingLoading, setSendingLoading] = useState(false); | ||||
| const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); | const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); | ||||
| const { fetchRelatedQuestions, data: relatedQuestions } = | const { fetchRelatedQuestions, data: relatedQuestions } = | ||||
| useFetchRelatedQuestions(); | |||||
| useFetchRelatedQuestions(tenantId); | |||||
| const [searchStr, setSearchStr] = useState<string>(''); | const [searchStr, setSearchStr] = useState<string>(''); | ||||
| const [isFirstRender, setIsFirstRender] = useState(true); | const [isFirstRender, setIsFirstRender] = useState(true); | ||||
| const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]); | const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]); | ||||
| setIsFirstRender(false); | setIsFirstRender(false); | ||||
| setCurrentAnswer({} as IAnswer); | setCurrentAnswer({} as IAnswer); | ||||
| setSendingLoading(true); | setSendingLoading(true); | ||||
| send({ kb_ids: kbIds, question: q }); | |||||
| send({ kb_ids: kbIds, question: q, tenantId }); | |||||
| testChunk({ | testChunk({ | ||||
| kb_id: kbIds, | kb_id: kbIds, | ||||
| highlight: true, | highlight: true, | ||||
| fetchRelatedQuestions, | fetchRelatedQuestions, | ||||
| setPagination, | setPagination, | ||||
| pagination.pageSize, | pagination.pageSize, | ||||
| tenantId, | |||||
| ], | ], | ||||
| ); | ); | ||||
| fetchMindMap, | fetchMindMap, | ||||
| data: mindMap, | data: mindMap, | ||||
| loading: mindMapLoading, | loading: mindMapLoading, | ||||
| } = useFetchMindMap(); | |||||
| } = useSearchFetchMindMap(); | |||||
| const handleShowModal = useCallback(() => { | const handleShowModal = useCallback(() => { | ||||
| const searchParams = { question: trim(question), kb_ids: kbIds }; | const searchParams = { question: trim(question), kb_ids: kbIds }; | 
| AgentList = '/agent-list', | AgentList = '/agent-list', | ||||
| Searches = '/next-searches', | Searches = '/next-searches', | ||||
| Search = '/next-search', | Search = '/next-search', | ||||
| SearchShare = '/next-search/share', | |||||
| Chats = '/next-chats', | Chats = '/next-chats', | ||||
| Chat = '/next-chat', | Chat = '/next-chat', | ||||
| Files = '/files', | Files = '/files', | ||||
| layout: false, | layout: false, | ||||
| component: `@/pages${Routes.Search}`, | component: `@/pages${Routes.Search}`, | ||||
| }, | }, | ||||
| { | |||||
| path: `${Routes.SearchShare}`, | |||||
| layout: false, | |||||
| component: `@/pages${Routes.SearchShare}`, | |||||
| }, | |||||
| { | { | ||||
| path: Routes.Agents, | path: Routes.Agents, | ||||
| layout: false, | layout: false, | 
| listTagByKnowledgeIds, | listTagByKnowledgeIds, | ||||
| setMeta, | setMeta, | ||||
| getMeta, | getMeta, | ||||
| retrievalTestShare, | |||||
| } = api; | } = api; | ||||
| const methods = { | const methods = { | ||||
| url: getMeta, | url: getMeta, | ||||
| method: 'get', | method: 'get', | ||||
| }, | }, | ||||
| retrievalTestShare: { | |||||
| url: retrievalTestShare, | |||||
| method: 'post', | |||||
| }, | |||||
| }; | }; | ||||
| const kbService = registerServer<keyof typeof methods>(methods, request); | const kbService = registerServer<keyof typeof methods>(methods, request); | 
| deleteSearch, | deleteSearch, | ||||
| getSearchDetail, | getSearchDetail, | ||||
| updateSearchSetting, | updateSearchSetting, | ||||
| askShare, | |||||
| mindmapShare, | |||||
| getRelatedQuestionsShare, | |||||
| getSearchDetailShare, | |||||
| } = api; | } = api; | ||||
| const methods = { | const methods = { | ||||
| createSearch: { | createSearch: { | ||||
| url: updateSearchSetting, | url: updateSearchSetting, | ||||
| method: 'post', | method: 'post', | ||||
| }, | }, | ||||
| askShare: { | |||||
| url: askShare, | |||||
| method: 'post', | |||||
| }, | |||||
| mindmapShare: { | |||||
| url: mindmapShare, | |||||
| method: 'post', | |||||
| }, | |||||
| getRelatedQuestionsShare: { | |||||
| url: getRelatedQuestionsShare, | |||||
| method: 'post', | |||||
| }, | |||||
| getSearchDetailShare: { | |||||
| url: getSearchDetailShare, | |||||
| method: 'get', | |||||
| }, | |||||
| } as const; | } as const; | ||||
| const searchService = registerServer<keyof typeof methods>(methods, request); | const searchService = registerServer<keyof typeof methods>(methods, request); | ||||
| getSearchList: `${api_host}/search/list`, | getSearchList: `${api_host}/search/list`, | ||||
| deleteSearch: `${api_host}/search/rm`, | deleteSearch: `${api_host}/search/rm`, | ||||
| getSearchDetail: `${api_host}/search/detail`, | getSearchDetail: `${api_host}/search/detail`, | ||||
| getSearchDetailShare: `${ExternalApi}${api_host}/searchbots/detail`, | |||||
| updateSearchSetting: `${api_host}/search/update`, | 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`, | |||||
| }; | }; |