diff --git a/web/src/constants/chat.ts b/web/src/constants/chat.ts index 53f99b15a..d6777fd72 100644 --- a/web/src/constants/chat.ts +++ b/web/src/constants/chat.ts @@ -22,6 +22,7 @@ export const variableEnabledFieldMap = { export enum SharedFrom { Agent = 'agent', Chat = 'chat', + Search = 'search', } export enum ChatSearchParams { diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 51d7452d5..ce28a1e09 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -25,7 +25,7 @@ import { useDebounce } from 'ahooks'; import { get, set } from 'lodash'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'umi'; +import { useParams, useSearchParams } from 'umi'; import { v4 as uuid } from 'uuid'; import { useGetPaginationWithRouter, @@ -304,6 +304,9 @@ export const useSetAgent = (showMessage: boolean = true) => { // Only one file can be uploaded at a time export const useUploadCanvasFile = () => { const { id } = useParams(); + const [searchParams] = useSearchParams(); + const shared_id = searchParams.get('shared_id'); + const canvasId = id || shared_id; const { data, isPending: loading, @@ -321,7 +324,7 @@ export const useUploadCanvasFile = () => { } const { data } = await agentService.uploadCanvasFile( - { url: api.uploadAgentFile(id), data: nextBody }, + { url: api.uploadAgentFile(canvasId as string), data: nextBody }, true, ); if (data?.code === 0) { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 6e179bcc5..a23d2c5b0 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1418,6 +1418,7 @@ This delimiter is used to split the input text into several text pieces echo of }, search: { createSearch: 'Create Search', + searchGreeting: 'How can I help you today ?', }, }, }; diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 836478527..c91f49aa3 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -1185,9 +1185,13 @@ export default { knowledge: '知識', chat: '聊天', }, - }, - modal: { - okText: '確認', - cancelText: '取消', + modal: { + okText: '確認', + cancelText: '取消', + }, + search: { + createSearch: '新建查詢', + searchGreeting: '今天我能為你做些什麽?', + }, }, }; diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 90cc053f3..3b37c3ba2 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1316,12 +1316,13 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 }, }, }, - }, - modal: { - okText: '确认', - cancelText: '取消', - }, - search: { - createSearch: '新建查询', + modal: { + okText: '确认', + cancelText: '取消', + }, + search: { + createSearch: '新建查询', + searchGreeting: '今天我能为你做些什么?', + }, }, }; diff --git a/web/src/pages/agent/hooks/use-show-dialog.ts b/web/src/pages/agent/hooks/use-show-dialog.ts index 16878d84d..725d97031 100644 --- a/web/src/pages/agent/hooks/use-show-dialog.ts +++ b/web/src/pages/agent/hooks/use-show-dialog.ts @@ -85,7 +85,7 @@ const getUrlWithToken = (token: string, from: string = 'chat') => { return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`; }; -const useFetchTokenListBeforeOtherStep = () => { +export const useFetchTokenListBeforeOtherStep = () => { const { showTokenEmptyError } = useShowTokenEmptyError(); const { showBetaEmptyError } = useShowBetaEmptyError(); diff --git a/web/src/pages/next-search/document-preview-modal/index.tsx b/web/src/pages/next-search/document-preview-modal/index.tsx index a803a907e..8e3419d73 100644 --- a/web/src/pages/next-search/document-preview-modal/index.tsx +++ b/web/src/pages/next-search/document-preview-modal/index.tsx @@ -12,7 +12,8 @@ import { useEffect, useState } from 'react'; interface IProps extends IModalProps { 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 ( - - {chunk.docnm_kwd} + + {chunk.docnm_kwd || chunk.document_name} } onCancel={hideModal} diff --git a/web/src/pages/next-search/embed-app-modal.tsx b/web/src/pages/next-search/embed-app-modal.tsx new file mode 100644 index 000000000..83ad52940 --- /dev/null +++ b/web/src/pages/next-search/embed-app-modal.tsx @@ -0,0 +1,140 @@ +import HightLightMarkdown from '@/components/highlight-markdown'; +import { Modal } from '@/components/ui/modal/modal'; +import { RAGFlowSelect } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { + LanguageAbbreviation, + LanguageAbbreviationMap, +} from '@/constants/common'; +import { useTranslate } from '@/hooks/common-hooks'; +import { useCallback, useMemo, useState } from 'react'; + +type IEmbedAppModalProps = { + open: any; + url: string; + token: string; + from: string; + beta: string; + setOpen: (e: any) => void; + tenantId: string; +}; + +const EmbedAppModal = (props: IEmbedAppModalProps) => { + const { t } = useTranslate('chat'); + const { open, setOpen, token = '', from, beta = '', url, tenantId } = props; + const [hideAvatar, setHideAvatar] = useState(false); + const [locale, setLocale] = useState(''); + + const languageOptions = useMemo(() => { + return Object.values(LanguageAbbreviation).map((x) => ({ + label: LanguageAbbreviationMap[x], + value: x, + })); + }, []); + + const generateIframeSrc = useCallback(() => { + // const { visibleAvatar, locale } = values; + let src = `${location.origin}${url}?shared_id=${token}&from=${from}&auth=${beta}&tenantId=${tenantId}`; + if (hideAvatar) { + src += '&visible_avatar=1'; + } + if (locale) { + src += `&locale=${locale}`; + } + return src; + }, [beta, from, token, hideAvatar, locale, url, tenantId]); + + // ... existing code ... + const text = useMemo(() => { + const iframeSrc = generateIframeSrc(); + return `\`\`\`html + +\`\`\``; + }, [generateIframeSrc]); + // ... existing code ... + return ( + setOpen(false)} + showfooter={false} + footer={null} + > +
+ {/* Hide Avatar Toggle */} +
+ +
+ { + setHideAvatar(value); + }} + /> +
+
+ + {/* Locale Select */} +
+ + setLocale(value)} + options={languageOptions} + > +
+ {/* Embed Code */} +
+ + {/*
*/} + {/*
{text}
*/} + {text} + {/*
*/} +
+ + {/* ID Field */} +
+ +
+ + +
+
+
+
+ ); +}; +export default EmbedAppModal; diff --git a/web/src/pages/next-search/hooks.ts b/web/src/pages/next-search/hooks.ts new file mode 100644 index 000000000..348a56c40 --- /dev/null +++ b/web/src/pages/next-search/hooks.ts @@ -0,0 +1,488 @@ +import message from '@/components/ui/message'; +import { SharedFrom } from '@/constants/chat'; +import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; +import { + useGetPaginationWithRouter, + useSendMessageWithSse, +} from '@/hooks/logic-hooks'; +import { useSetPaginationParams } from '@/hooks/route-hook'; +import { useKnowledgeBaseId } from '@/hooks/use-knowledge-request'; +import { ResponsePostType } from '@/interfaces/database/base'; +import { IAnswer } from '@/interfaces/database/chat'; +import { ITestingResult } from '@/interfaces/database/knowledge'; +import { IAskRequestBody } from '@/interfaces/request/chat'; +import chatService from '@/services/chat-service'; +import kbService from '@/services/knowledge-service'; +import searchService from '@/services/search-service'; +import api from '@/utils/api'; +import { useMutation } from '@tanstack/react-query'; +import { has, isEmpty, trim } from 'lodash'; +import { + ChangeEventHandler, + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; +import { useSearchParams } from 'umi'; +import { ISearchAppDetailProps } from '../next-searches/hooks'; +import { useShowMindMapDrawer } from '../search/hooks'; +import { useClickDrawer } from './document-preview-modal/hooks'; + +export interface ISearchingProps { + searchText?: string; + data: ISearchAppDetailProps; + setIsSearching?: Dispatch>; + setSearchText?: Dispatch>; +} + +export type ISearchReturnProps = ReturnType; + +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 & { + 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 & { + 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([]); + + 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 => { + 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(''); + const [isFirstRender, setIsFirstRender] = useState(true); + const [selectedDocumentIds, setSelectedDocumentIds] = useState([]); + + 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 = + 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, + }; +}; diff --git a/web/src/pages/next-search/index.tsx b/web/src/pages/next-search/index.tsx index 31870110f..90d150067 100644 --- a/web/src/pages/next-search/index.tsx +++ b/web/src/pages/next-search/index.tsx @@ -8,13 +8,17 @@ import { BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; +import { SharedFrom } from '@/constants/chat'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; -import { Settings } from 'lucide-react'; +import { useFetchTenantInfo } from '@/hooks/user-setting-hooks'; +import { Send, Settings } from 'lucide-react'; import { useEffect, useState } from 'react'; +import { useFetchTokenListBeforeOtherStep } from '../agent/hooks/use-show-dialog'; import { ISearchAppDetailProps, useFetchSearchDetail, } from '../next-searches/hooks'; +import EmbedAppModal from './embed-app-modal'; import './index.less'; import SearchHome from './search-home'; import { SearchSetting } from './search-setting'; @@ -24,9 +28,15 @@ export default function SearchPage() { const { navigateToSearchList } = useNavigatePage(); const [isSearching, setIsSearching] = useState(false); const { data: SearchData } = useFetchSearchDetail(); - + const { beta, handleOperate } = useFetchTokenListBeforeOtherStep(); const [openSetting, setOpenSetting] = useState(false); + const [openEmbed, setOpenEmbed] = useState(false); const [searchText, setSearchText] = useState(''); + const { data: tenantInfo } = useFetchTenantInfo(); + const tenantId = tenantInfo.tenant_id; + useEffect(() => { + handleOperate(); + }, [handleOperate]); useEffect(() => { if (isSearching) { setOpenSetting(false); @@ -81,8 +91,37 @@ export default function SearchPage() { data={SearchData as ISearchAppDetailProps} /> )} + { + + } + { + // + } + +
+
- {!isSearching && (
- - - - -
- ); diff --git a/web/src/pages/next-search/search-setting.tsx b/web/src/pages/next-search/search-setting.tsx index 0abe13776..5b028d811 100644 --- a/web/src/pages/next-search/search-setting.tsx +++ b/web/src/pages/next-search/search-setting.tsx @@ -12,7 +12,6 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { Label } from '@/components/ui/label'; import { MultiSelect, MultiSelectOptionType, @@ -42,6 +41,7 @@ import { import { ISearchAppDetailProps, IUpdateSearchProps, + IllmSettingProps, useUpdateSearch, } from '../next-searches/hooks'; import { @@ -55,14 +55,6 @@ interface SearchSettingProps { className?: string; data: ISearchAppDetailProps; } -interface ISubmitLlmSettingProps { - llm_id: string; - parameter: string; - temperature?: number; - top_p?: number; - frequency_penalty?: number; - presence_penalty?: number; -} const SearchSettingFormSchema = z .object({ @@ -120,16 +112,19 @@ const SearchSetting: React.FC = ({ const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 const [datasetList, setDatasetList] = useState([]); 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 = ({ })(); } }, [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 = ({ ) => { 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 = ({ 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 = ({ ...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 = ({ Avatar -
- {!avatarBase64Str ? ( -
-
- -

{t('common.upload')}

+
+
+ {!avatarBase64Str ? ( +
+
+ +

{t('common.upload')}

+
-
- ) : ( -
- -
- + +
+ +
-
- )} - { - const file = ev.target?.files?.[0]; - if ( - /\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '') - ) { - setAvatarFile(file!); - } - ev.target.value = ''; - }} - /> + )} + { + const file = ev.target?.files?.[0]; + if ( + /\.(jpg|jpeg|png|webp|bmp)$/i.test( + file?.name ?? '', + ) + ) { + setAvatarFile(file!); + } + ev.target.value = ''; + }} + /> +
+ +
+ {t('knowledgeConfiguration.photoTip')} +
@@ -410,7 +415,20 @@ const SearchSetting: React.FC = ({ Description - + { + if (field.value === descriptionDefaultValue) { + field.onChange(''); + } + }} + onBlur={() => { + if (field.value === '') { + field.onChange(descriptionDefaultValue); + } + }} + /> @@ -451,26 +469,58 @@ const SearchSetting: React.FC = ({ control={formMethods.control} name="search_config.vector_similarity_weight" render={({ field }) => ( - + *Keyword Similarity Weight - -
+
+ field.onChange(values)} + {...field} + max={1} + min={0} + step={0.01} > - -
- + + + + +
+ // + // + // *Keyword + // Similarity Weight + // + // + // {/*
+ // field.onChange(values)} + // > + // + //
*/} + //
+ // + //
)} /> @@ -528,16 +578,15 @@ const SearchSetting: React.FC = ({ >; + 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(''); + + useEffect(() => { + setSearchtext(searchStr); + }, [searchStr, setSearchtext]); + return ( +
+ {/* search header */} +
+

{ + setIsSearching?.(false); + }} + > + RAGFlow +

+ +
+
+
+ { + setSearchtext(e.target.value); + }} + disabled={sendingLoading} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(searchtext); + } + }} + /> +
+ { + handleClickRelatedQuestion(''); + }} + /> + | + +
+
+
+ {/* search body */} +
+ {searchData.search_config.summary && !isSearchStrEmpty && ( + <> +
+ AI Summary +
+ {isEmpty(answer) && sendingLoading ? ( +
+ + + +
+ ) : ( + answer.answer && ( +
+ +
+ ) + )} +
+ + )} + {/* retrieval documents */} + {!isSearchStrEmpty && ( + <> +
+ +
+
+ + )} +
+ + {chunks?.length > 0 && ( + <> + {chunks.map((chunk, index) => { + return ( + <> +
+
+ + + +
+
+ + + {chunk.content_with_weight} + + +
+
+
+ clickDocumentButton(chunk.doc_id, chunk as any) + } + > + + {chunk.docnm_kwd} +
+
+ {index < chunks.length - 1 && ( +
+ )} + + ); + })} + + )} +
+ {relatedQuestions?.length > 0 && ( +
+

+ Related Search +

+
+ {relatedQuestions?.map((x, idx) => ( + + ))} +
+
+ )} +
+
+ + {total > 0 && ( +
+ +
+ )} +
+ + {mindMapVisible && ( +
+ +
+ )} +
+ {!mindMapVisible && + !isFirstRender && + !isSearchStrEmpty && + !isEmpty(searchData.search_config.kb_ids) && + searchData.search_config.query_mindmap && ( + + +
+ {/* */} + +
+
+ {t('chunk.mind')} +
+ )} + {visible && ( + + )} +
+ ); +} diff --git a/web/src/pages/next-search/searching.tsx b/web/src/pages/next-search/searching.tsx index 934c66e98..d4036a4e8 100644 --- a/web/src/pages/next-search/searching.tsx +++ b/web/src/pages/next-search/searching.tsx @@ -1,332 +1,33 @@ -import { FileIcon } from '@/components/icon-font'; -import { ImageWithPopover } from '@/components/image'; -import { Input } from '@/components/originui/input'; -import { useClickDrawer } from '@/components/pdf-drawer/hooks'; -import { Button } from '@/components/ui/button'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; -import { Skeleton } from '@/components/ui/skeleton'; -import { Spin } from '@/components/ui/spin'; -import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; -import { useGetPaginationWithRouter } from '@/hooks/logic-hooks'; -import { IReference } from '@/interfaces/database/chat'; -import { cn } from '@/lib/utils'; -import DOMPurify from 'dompurify'; -import { t } from 'i18next'; -import { isEmpty } from 'lodash'; -import { BrainCircuit, Search, Square, Tag, X } from 'lucide-react'; -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useRef, -} from 'react'; +import { Dispatch, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; import { ISearchAppDetailProps } from '../next-searches/hooks'; -import { useSendQuestion, useShowMindMapDrawer } from '../search/hooks'; -import PdfDrawer from './document-preview-modal'; -import HightLightMarkdown from './highlight-markdown'; +import { useSearching } from './hooks'; import './index.less'; -import styles from './index.less'; -import MarkdownContent from './markdown-content'; -import MindMapDrawer from './mindmap-drawer'; -import RetrievalDocuments from './retrieval-documents'; +import SearchingView from './search-view'; export default function SearchingPage({ searchText, data: searchData, setIsSearching, + setSearchText, }: { searchText: string; setIsSearching: Dispatch>; setSearchText: Dispatch>; data: ISearchAppDetailProps; }) { - const inputRef = useRef(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 ( -
- {/* search header */} -
-

{ - setIsSearching(false); - }} - > - RAGFlow -

- -
-
-
- { - if (e.key === 'Enter') { - handleSearch(); - } - }} - /> -
- { - handleClickRelatedQuestion(''); - }} - /> - | - -
-
-
- - {/* search body */} -
- {searchData.search_config.summary && ( - <> -
- AI Summary -
- {isEmpty(answer) && sendingLoading ? ( -
- - - -
- ) : ( - answer.answer && ( -
- -
- ) - )} - - )} - -
- {/* retrieval documents */} -
- -
-
-
- - {chunks?.length > 0 && ( - <> - {chunks.map((chunk, index) => { - return ( - <> -
-
- - - -
-
- - - {chunk.content_with_weight} - - -
-
-
- clickDocumentButton(chunk.doc_id, chunk as any) - } - > - - {chunk.docnm_kwd} -
-
- {index < chunks.length - 1 && ( -
- )} - - ); - })} - - )} -
- {relatedQuestions?.length > 0 && ( -
-
- {relatedQuestions?.map((x, idx) => ( - - {x} - - ))} -
-
- )} -
-
-
- -
-
-
- {!mindMapVisible && - !isFirstRender && - !isSearchStrEmpty && - !isEmpty(searchData.search_config.kb_ids) && ( - - - - - {t('chunk.mind')} - - )} - {visible && ( - - )} - {mindMapVisible && ( -
- -
- )} -
+ ); } diff --git a/web/src/pages/next-search/share/index.tsx b/web/src/pages/next-search/share/index.tsx new file mode 100644 index 000000000..9ecf8788e --- /dev/null +++ b/web/src/pages/next-search/share/index.tsx @@ -0,0 +1,35 @@ +import i18n from '@/locales/config'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ISearchAppDetailProps, + useFetchSearchDetail, +} from '../../next-searches/hooks'; +import { useGetSharedSearchParams, useSearching } from '../hooks'; +import '../index.less'; +import SearchingView from '../search-view'; +export default function SearchingPage() { + const { tenantId, locale } = useGetSharedSearchParams(); + const { + data: searchData = { + search_config: { kb_ids: [] }, + } as unknown as ISearchAppDetailProps, + } = useFetchSearchDetail(tenantId as string); + const searchingParam = useSearching({ + data: searchData, + }); + const { t } = useTranslation(); + + // useEffect(() => { + // if (locale) { + // i18n.changeLanguage(locale); + // } + // }, [locale, i18n]); + useEffect(() => { + console.log('locale', locale, i18n.language); + if (locale && i18n.language !== locale) { + i18n.changeLanguage(locale); + } + }, [locale]); + return ; +} diff --git a/web/src/pages/next-searches/hooks.ts b/web/src/pages/next-searches/hooks.ts index a38720561..ac20d6541 100644 --- a/web/src/pages/next-searches/hooks.ts +++ b/web/src/pages/next-searches/hooks.ts @@ -5,7 +5,7 @@ import searchService from '@/services/search-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'umi'; +import { useParams, useSearchParams } from 'umi'; interface CreateSearchProps { name: string; @@ -156,13 +156,13 @@ export const useDeleteSearch = () => { return { data, isError, deleteSearch }; }; -interface IllmSettingProps { +export interface IllmSettingProps { llm_id: string; parameter: string; - temperature: number; - top_p: number; - frequency_penalty: number; - presence_penalty: number; + temperature?: number; + top_p?: number; + frequency_penalty?: number; + presence_penalty?: number; } interface IllmSettingEnableProps { temperatureEnabled?: boolean; @@ -204,14 +204,29 @@ interface SearchDetailResponse { message: string; } -export const useFetchSearchDetail = () => { +export const useFetchSearchDetail = (tenantId?: string) => { const { id } = useParams(); + + const [searchParams] = useSearchParams(); + const shared_id = searchParams.get('shared_id'); + const searchId = id || shared_id; + let param: { search_id: string | null; tenant_id?: string } = { + search_id: searchId, + }; + if (shared_id) { + param = { + search_id: searchId, + tenant_id: tenantId, + }; + } + const fetchSearchDetailFunc = shared_id + ? searchService.getSearchDetailShare + : searchService.getSearchDetail; const { data, isLoading, isError } = useQuery({ - 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'); } diff --git a/web/src/pages/next-searches/index.tsx b/web/src/pages/next-searches/index.tsx index 453af7aff..017e10c47 100644 --- a/web/src/pages/next-searches/index.tsx +++ b/web/src/pages/next-searches/index.tsx @@ -66,7 +66,7 @@ export default function SearchList() { setSearchListParams({ ...searchParams, page, page_size: pageSize }); }; return ( -
+
-
- {list?.data.search_apps.map((x) => { - return ; - })} +
+
+ {list?.data.search_apps.map((x) => { + return ; + })} +
- {list?.data.total && ( - + {list?.data.total && list?.data.total > 0 && ( +
+ +
)} + { diff --git a/web/src/pages/next-searches/search-card.tsx b/web/src/pages/next-searches/search-card.tsx index 830e92aaa..caafda954 100644 --- a/web/src/pages/next-searches/search-card.tsx +++ b/web/src/pages/next-searches/search-card.tsx @@ -19,7 +19,7 @@ export function SearchCard({ data }: IProps) { navigateToSearch(data?.id); }} > - +
-
+
{data.name} @@ -37,22 +37,13 @@ export function SearchCard({ data }: IProps) {
-
{data.description}
-
+
+
{data.description}
- Search app

{formatDate(data.update_time)}

- {/*
- - -
*/}
diff --git a/web/src/pages/search/hooks.ts b/web/src/pages/search/hooks.ts index 7ce26ac8f..8c7607972 100644 --- a/web/src/pages/search/hooks.ts +++ b/web/src/pages/search/hooks.ts @@ -1,4 +1,4 @@ -import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks'; +import { useFetchRelatedQuestions } from '@/hooks/chat-hooks'; import { useSetModalState } from '@/hooks/common-hooks'; import { useTestChunkAllRetrieval, @@ -18,17 +18,23 @@ import { useRef, useState, } from 'react'; +import { + useGetSharedSearchParams, + useSearchFetchMindMap, +} from '../next-search/hooks'; -export const useSendQuestion = (kbIds: string[]) => { +export const useSendQuestion = (kbIds: string[], tenantId?: string) => { + const { sharedId } = useGetSharedSearchParams(); const { send, answer, done, stopOutputMessage } = useSendMessageWithSse( - api.ask, + sharedId ? api.askShare : api.ask, ); - const { testChunk, loading } = useTestChunkRetrieval(); - const { testChunkAll } = useTestChunkAllRetrieval(); + + const { testChunk, loading } = useTestChunkRetrieval(tenantId); + const { testChunkAll } = useTestChunkAllRetrieval(tenantId); const [sendingLoading, setSendingLoading] = useState(false); const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); const { fetchRelatedQuestions, data: relatedQuestions } = - useFetchRelatedQuestions(); + useFetchRelatedQuestions(tenantId); const [searchStr, setSearchStr] = useState(''); const [isFirstRender, setIsFirstRender] = useState(true); const [selectedDocumentIds, setSelectedDocumentIds] = useState([]); @@ -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 }; diff --git a/web/src/routes.ts b/web/src/routes.ts index f7c0874d9..563f12416 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -11,6 +11,7 @@ export enum Routes { AgentList = '/agent-list', Searches = '/next-searches', Search = '/next-search', + SearchShare = '/next-search/share', Chats = '/next-chats', Chat = '/next-chat', Files = '/files', @@ -234,6 +235,11 @@ const routes = [ layout: false, component: `@/pages${Routes.Search}`, }, + { + path: `${Routes.SearchShare}`, + layout: false, + component: `@/pages${Routes.SearchShare}`, + }, { path: Routes.Agents, layout: false, diff --git a/web/src/services/knowledge-service.ts b/web/src/services/knowledge-service.ts index 15d3d9462..8d0bb8d30 100644 --- a/web/src/services/knowledge-service.ts +++ b/web/src/services/knowledge-service.ts @@ -38,6 +38,7 @@ const { listTagByKnowledgeIds, setMeta, getMeta, + retrievalTestShare, } = api; const methods = { @@ -164,6 +165,10 @@ const methods = { url: getMeta, method: 'get', }, + retrievalTestShare: { + url: retrievalTestShare, + method: 'post', + }, }; const kbService = registerServer(methods, request); diff --git a/web/src/services/search-service.ts b/web/src/services/search-service.ts index 21af31d31..0b5bb4172 100644 --- a/web/src/services/search-service.ts +++ b/web/src/services/search-service.ts @@ -8,6 +8,10 @@ const { deleteSearch, getSearchDetail, updateSearchSetting, + askShare, + mindmapShare, + getRelatedQuestionsShare, + getSearchDetailShare, } = api; const methods = { createSearch: { @@ -27,6 +31,23 @@ const methods = { url: updateSearchSetting, method: 'post', }, + askShare: { + url: askShare, + method: 'post', + }, + mindmapShare: { + url: mindmapShare, + method: 'post', + }, + getRelatedQuestionsShare: { + url: getRelatedQuestionsShare, + method: 'post', + }, + + getSearchDetailShare: { + url: getSearchDetailShare, + method: 'get', + }, } as const; const searchService = registerServer(methods, request); diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 110f64969..1a57a6bbc 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -181,5 +181,10 @@ export default { getSearchList: `${api_host}/search/list`, deleteSearch: `${api_host}/search/rm`, getSearchDetail: `${api_host}/search/detail`, + getSearchDetailShare: `${ExternalApi}${api_host}/searchbots/detail`, updateSearchSetting: `${api_host}/search/update`, + askShare: `${ExternalApi}${api_host}/searchbots/ask`, + mindmapShare: `${ExternalApi}${api_host}/searchbots/mindmap`, + getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`, + retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`, };