mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-05-24 05:29:03 +08:00

### What problem does this PR solve? feat: Click on the relevant question tag to continue searching for answers #2247 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
605 lines
15 KiB
TypeScript
605 lines
15 KiB
TypeScript
import { Authorization } from '@/constants/authorization';
|
|
import { MessageType } from '@/constants/chat';
|
|
import { LanguageTranslationMap } from '@/constants/common';
|
|
import { Pagination } from '@/interfaces/common';
|
|
import { ResponseType } from '@/interfaces/database/base';
|
|
import { IAnswer, Message } from '@/interfaces/database/chat';
|
|
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
|
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
|
import { IClientConversation, IMessage } from '@/pages/chat/interface';
|
|
import api from '@/utils/api';
|
|
import { getAuthorization } from '@/utils/authorization-util';
|
|
import { buildMessageUuid, getMessagePureId } from '@/utils/chat';
|
|
import { PaginationProps, message } from 'antd';
|
|
import { FormInstance } from 'antd/lib';
|
|
import axios from 'axios';
|
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
import {
|
|
ChangeEventHandler,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useDispatch } from 'umi';
|
|
import { v4 as uuid } from 'uuid';
|
|
import { useSetModalState, useTranslate } from './common-hooks';
|
|
import { useSetDocumentParser } from './document-hooks';
|
|
import { useSetPaginationParams } from './route-hook';
|
|
import { useOneNamespaceEffectsLoading } from './store-hooks';
|
|
import { useFetchTenantInfo, useSaveSetting } from './user-setting-hooks';
|
|
|
|
export const useChangeDocumentParser = (documentId: string) => {
|
|
const setDocumentParser = useSetDocumentParser();
|
|
|
|
const {
|
|
visible: changeParserVisible,
|
|
hideModal: hideChangeParserModal,
|
|
showModal: showChangeParserModal,
|
|
} = useSetModalState();
|
|
const loading = useOneNamespaceEffectsLoading('kFModel', [
|
|
'document_change_parser',
|
|
]);
|
|
|
|
const onChangeParserOk = useCallback(
|
|
async (parserId: string, parserConfig: IChangeParserConfigRequestBody) => {
|
|
const ret = await setDocumentParser(parserId, documentId, parserConfig);
|
|
if (ret === 0) {
|
|
hideChangeParserModal();
|
|
}
|
|
},
|
|
[hideChangeParserModal, setDocumentParser, documentId],
|
|
);
|
|
|
|
return {
|
|
changeParserLoading: loading,
|
|
onChangeParserOk,
|
|
changeParserVisible,
|
|
hideChangeParserModal,
|
|
showChangeParserModal,
|
|
};
|
|
};
|
|
|
|
export const useSetSelectedRecord = <T = IKnowledgeFile>() => {
|
|
const [currentRecord, setCurrentRecord] = useState<T>({} as T);
|
|
|
|
const setRecord = (record: T) => {
|
|
setCurrentRecord(record);
|
|
};
|
|
|
|
return { currentRecord, setRecord };
|
|
};
|
|
|
|
export const useHandleSearchChange = () => {
|
|
const [searchString, setSearchString] = useState('');
|
|
|
|
const handleInputChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const value = e.target.value;
|
|
setSearchString(value);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return { handleInputChange, searchString };
|
|
};
|
|
|
|
export const useChangeLanguage = () => {
|
|
const { i18n } = useTranslation();
|
|
const { saveSetting } = useSaveSetting();
|
|
|
|
const changeLanguage = (lng: string) => {
|
|
i18n.changeLanguage(
|
|
LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap],
|
|
);
|
|
saveSetting({ language: lng });
|
|
};
|
|
|
|
return changeLanguage;
|
|
};
|
|
|
|
export const useGetPaginationWithRouter = () => {
|
|
const { t } = useTranslate('common');
|
|
const {
|
|
setPaginationParams,
|
|
page,
|
|
size: pageSize,
|
|
} = useSetPaginationParams();
|
|
|
|
const onPageChange: PaginationProps['onChange'] = useCallback(
|
|
(pageNumber: number, pageSize: number) => {
|
|
setPaginationParams(pageNumber, pageSize);
|
|
},
|
|
[setPaginationParams],
|
|
);
|
|
|
|
const setCurrentPagination = useCallback(
|
|
(pagination: { page: number; pageSize?: number }) => {
|
|
setPaginationParams(pagination.page, pagination.pageSize);
|
|
},
|
|
[setPaginationParams],
|
|
);
|
|
|
|
const pagination: PaginationProps = useMemo(() => {
|
|
return {
|
|
showQuickJumper: true,
|
|
total: 0,
|
|
showSizeChanger: true,
|
|
current: page,
|
|
pageSize: pageSize,
|
|
pageSizeOptions: [1, 2, 10, 20, 50, 100],
|
|
onChange: onPageChange,
|
|
showTotal: (total) => `${t('total')} ${total}`,
|
|
};
|
|
}, [t, onPageChange, page, pageSize]);
|
|
|
|
return {
|
|
pagination,
|
|
setPagination: setCurrentPagination,
|
|
};
|
|
};
|
|
|
|
export const useGetPagination = () => {
|
|
const [pagination, setPagination] = useState({ page: 1, pageSize: 10 });
|
|
const { t } = useTranslate('common');
|
|
|
|
const onPageChange: PaginationProps['onChange'] = useCallback(
|
|
(pageNumber: number, pageSize: number) => {
|
|
setPagination({ page: pageNumber, pageSize });
|
|
},
|
|
[],
|
|
);
|
|
|
|
const currentPagination: PaginationProps = useMemo(() => {
|
|
return {
|
|
showQuickJumper: true,
|
|
total: 0,
|
|
showSizeChanger: true,
|
|
current: pagination.page,
|
|
pageSize: pagination.pageSize,
|
|
pageSizeOptions: [1, 2, 10, 20, 50, 100],
|
|
onChange: onPageChange,
|
|
showTotal: (total) => `${t('total')} ${total}`,
|
|
};
|
|
}, [t, onPageChange, pagination]);
|
|
|
|
return {
|
|
pagination: currentPagination,
|
|
};
|
|
};
|
|
|
|
export const useSetPagination = (namespace: string) => {
|
|
const dispatch = useDispatch();
|
|
|
|
const setPagination = useCallback(
|
|
(pageNumber = 1, pageSize?: number) => {
|
|
const pagination: Pagination = {
|
|
current: pageNumber,
|
|
} as Pagination;
|
|
if (pageSize) {
|
|
pagination.pageSize = pageSize;
|
|
}
|
|
dispatch({
|
|
type: `${namespace}/setPagination`,
|
|
payload: pagination,
|
|
});
|
|
},
|
|
[dispatch, namespace],
|
|
);
|
|
|
|
return setPagination;
|
|
};
|
|
|
|
export interface AppConf {
|
|
appName: string;
|
|
}
|
|
|
|
export const useFetchAppConf = () => {
|
|
const [appConf, setAppConf] = useState<AppConf>({} as AppConf);
|
|
const fetchAppConf = useCallback(async () => {
|
|
const ret = await axios.get('/conf.json');
|
|
|
|
setAppConf(ret.data);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchAppConf();
|
|
}, [fetchAppConf]);
|
|
|
|
return appConf;
|
|
};
|
|
|
|
export const useSendMessageWithSse = (
|
|
url: string = api.completeConversation,
|
|
) => {
|
|
const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
|
|
const [done, setDone] = useState(true);
|
|
|
|
const resetAnswer = useCallback(() => {
|
|
setAnswer({} as IAnswer);
|
|
}, []);
|
|
|
|
const send = useCallback(
|
|
async (
|
|
body: any,
|
|
): Promise<{ response: Response; data: ResponseType } | undefined> => {
|
|
try {
|
|
setDone(false);
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
[Authorization]: getAuthorization(),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const res = response.clone().json();
|
|
|
|
const reader = response?.body
|
|
?.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(new EventSourceParserStream())
|
|
.getReader();
|
|
|
|
while (true) {
|
|
const x = await reader?.read();
|
|
if (x) {
|
|
const { done, value } = x;
|
|
if (done) {
|
|
console.info('done');
|
|
break;
|
|
}
|
|
try {
|
|
const val = JSON.parse(value?.data || '');
|
|
const d = val?.data;
|
|
if (typeof d !== 'boolean') {
|
|
console.info('data:', d);
|
|
setAnswer({
|
|
...d,
|
|
conversationId: body?.conversation_id,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
}
|
|
}
|
|
console.info('done?');
|
|
setDone(true);
|
|
return { data: await res, response };
|
|
} catch (e) {
|
|
setDone(true);
|
|
console.warn(e);
|
|
}
|
|
},
|
|
[url],
|
|
);
|
|
|
|
return { send, answer, done, setDone, resetAnswer };
|
|
};
|
|
|
|
export const useSpeechWithSse = (url: string = api.tts) => {
|
|
const read = useCallback(
|
|
async (body: any) => {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
[Authorization]: getAuthorization(),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
try {
|
|
const res = await response.clone().json();
|
|
if (res?.retcode !== 0) {
|
|
message.error(res?.retmsg);
|
|
}
|
|
} catch (error) {
|
|
console.warn('🚀 ~ error:', error);
|
|
}
|
|
return response;
|
|
},
|
|
[url],
|
|
);
|
|
|
|
return { read };
|
|
};
|
|
|
|
//#region chat hooks
|
|
|
|
export const useScrollToBottom = (messages?: unknown) => {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const scrollToBottom = useCallback(() => {
|
|
if (messages) {
|
|
ref.current?.scrollIntoView({ behavior: 'instant' });
|
|
}
|
|
}, [messages]); // If the message changes, scroll to the bottom
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [scrollToBottom]);
|
|
|
|
return ref;
|
|
};
|
|
|
|
export const useHandleMessageInputChange = () => {
|
|
const [value, setValue] = useState('');
|
|
|
|
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
const value = e.target.value;
|
|
const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t');
|
|
setValue(nextValue);
|
|
};
|
|
|
|
return {
|
|
handleInputChange,
|
|
value,
|
|
setValue,
|
|
};
|
|
};
|
|
|
|
export const useSelectDerivedMessages = () => {
|
|
const [derivedMessages, setDerivedMessages] = useState<IMessage[]>([]);
|
|
|
|
const ref = useScrollToBottom(derivedMessages);
|
|
|
|
const addNewestQuestion = useCallback(
|
|
(message: Message, answer: string = '') => {
|
|
setDerivedMessages((pre) => {
|
|
return [
|
|
...pre,
|
|
{
|
|
...message,
|
|
id: buildMessageUuid(message),
|
|
},
|
|
{
|
|
role: MessageType.Assistant,
|
|
content: answer,
|
|
id: buildMessageUuid({ ...message, role: MessageType.Assistant }),
|
|
},
|
|
];
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
// Add the streaming message to the last item in the message list
|
|
const addNewestAnswer = useCallback((answer: IAnswer) => {
|
|
setDerivedMessages((pre) => {
|
|
return [
|
|
...(pre?.slice(0, -1) ?? []),
|
|
{
|
|
role: MessageType.Assistant,
|
|
content: answer.answer,
|
|
reference: answer.reference,
|
|
id: buildMessageUuid({
|
|
id: answer.id,
|
|
role: MessageType.Assistant,
|
|
}),
|
|
prompt: answer.prompt,
|
|
audio_binary: answer.audio_binary,
|
|
},
|
|
];
|
|
});
|
|
}, []);
|
|
|
|
const removeLatestMessage = useCallback(() => {
|
|
setDerivedMessages((pre) => {
|
|
const nextMessages = pre?.slice(0, -2) ?? [];
|
|
return nextMessages;
|
|
});
|
|
}, []);
|
|
|
|
const removeMessageById = useCallback(
|
|
(messageId: string) => {
|
|
setDerivedMessages((pre) => {
|
|
const nextMessages =
|
|
pre?.filter(
|
|
(x) => getMessagePureId(x.id) !== getMessagePureId(messageId),
|
|
) ?? [];
|
|
return nextMessages;
|
|
});
|
|
},
|
|
[setDerivedMessages],
|
|
);
|
|
|
|
const removeMessagesAfterCurrentMessage = useCallback(
|
|
(messageId: string) => {
|
|
setDerivedMessages((pre) => {
|
|
const index = pre.findIndex((x) => x.id === messageId);
|
|
if (index !== -1) {
|
|
let nextMessages = pre.slice(0, index + 2) ?? [];
|
|
const latestMessage = nextMessages.at(-1);
|
|
nextMessages = latestMessage
|
|
? [
|
|
...nextMessages.slice(0, -1),
|
|
{
|
|
...latestMessage,
|
|
content: '',
|
|
reference: undefined,
|
|
prompt: undefined,
|
|
},
|
|
]
|
|
: nextMessages;
|
|
return nextMessages;
|
|
}
|
|
return pre;
|
|
});
|
|
},
|
|
[setDerivedMessages],
|
|
);
|
|
|
|
return {
|
|
ref,
|
|
derivedMessages,
|
|
setDerivedMessages,
|
|
addNewestQuestion,
|
|
addNewestAnswer,
|
|
removeLatestMessage,
|
|
removeMessageById,
|
|
removeMessagesAfterCurrentMessage,
|
|
};
|
|
};
|
|
|
|
export interface IRemoveMessageById {
|
|
removeMessageById(messageId: string): void;
|
|
}
|
|
|
|
export const useRemoveMessageById = (
|
|
setCurrentConversation: (
|
|
callback: (state: IClientConversation) => IClientConversation,
|
|
) => void,
|
|
) => {
|
|
const removeMessageById = useCallback(
|
|
(messageId: string) => {
|
|
setCurrentConversation((pre) => {
|
|
const nextMessages =
|
|
pre.message?.filter(
|
|
(x) => getMessagePureId(x.id) !== getMessagePureId(messageId),
|
|
) ?? [];
|
|
return {
|
|
...pre,
|
|
message: nextMessages,
|
|
};
|
|
});
|
|
},
|
|
[setCurrentConversation],
|
|
);
|
|
|
|
return { removeMessageById };
|
|
};
|
|
|
|
export const useRemoveMessagesAfterCurrentMessage = (
|
|
setCurrentConversation: (
|
|
callback: (state: IClientConversation) => IClientConversation,
|
|
) => void,
|
|
) => {
|
|
const removeMessagesAfterCurrentMessage = useCallback(
|
|
(messageId: string) => {
|
|
setCurrentConversation((pre) => {
|
|
const index = pre.message?.findIndex((x) => x.id === messageId);
|
|
if (index !== -1) {
|
|
let nextMessages = pre.message?.slice(0, index + 2) ?? [];
|
|
const latestMessage = nextMessages.at(-1);
|
|
nextMessages = latestMessage
|
|
? [
|
|
...nextMessages.slice(0, -1),
|
|
{
|
|
...latestMessage,
|
|
content: '',
|
|
reference: undefined,
|
|
prompt: undefined,
|
|
},
|
|
]
|
|
: nextMessages;
|
|
return {
|
|
...pre,
|
|
message: nextMessages,
|
|
};
|
|
}
|
|
return pre;
|
|
});
|
|
},
|
|
[setCurrentConversation],
|
|
);
|
|
|
|
return { removeMessagesAfterCurrentMessage };
|
|
};
|
|
|
|
export interface IRegenerateMessage {
|
|
regenerateMessage?: (message: Message) => void;
|
|
}
|
|
|
|
export const useRegenerateMessage = ({
|
|
removeMessagesAfterCurrentMessage,
|
|
sendMessage,
|
|
messages,
|
|
}: {
|
|
removeMessagesAfterCurrentMessage(messageId: string): void;
|
|
sendMessage({
|
|
message,
|
|
}: {
|
|
message: Message;
|
|
messages?: Message[];
|
|
}): void | Promise<any>;
|
|
messages: Message[];
|
|
}) => {
|
|
const regenerateMessage = useCallback(
|
|
async (message: Message) => {
|
|
if (message.id) {
|
|
removeMessagesAfterCurrentMessage(message.id);
|
|
const index = messages.findIndex((x) => x.id === message.id);
|
|
let nextMessages;
|
|
if (index !== -1) {
|
|
nextMessages = messages.slice(0, index);
|
|
}
|
|
sendMessage({
|
|
message: { ...message, id: uuid() },
|
|
messages: nextMessages,
|
|
});
|
|
}
|
|
},
|
|
[removeMessagesAfterCurrentMessage, sendMessage, messages],
|
|
);
|
|
|
|
return { regenerateMessage };
|
|
};
|
|
|
|
// #endregion
|
|
|
|
/**
|
|
*
|
|
* @param defaultId
|
|
* used to switch between different items, similar to radio
|
|
* @returns
|
|
*/
|
|
export const useSelectItem = (defaultId?: string) => {
|
|
const [selectedId, setSelectedId] = useState('');
|
|
|
|
const handleItemClick = useCallback(
|
|
(id: string) => () => {
|
|
setSelectedId(id);
|
|
},
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (defaultId) {
|
|
setSelectedId(defaultId);
|
|
}
|
|
}, [defaultId]);
|
|
|
|
return { selectedId, handleItemClick };
|
|
};
|
|
|
|
export const useFetchModelId = () => {
|
|
const { data: tenantInfo } = useFetchTenantInfo();
|
|
|
|
return tenantInfo?.llm_id ?? '';
|
|
};
|
|
|
|
const ChunkTokenNumMap = {
|
|
naive: 128,
|
|
knowledge_graph: 8192,
|
|
};
|
|
|
|
export const useHandleChunkMethodSelectChange = (form: FormInstance) => {
|
|
// const form = Form.useFormInstance();
|
|
const handleChange = useCallback(
|
|
(value: string) => {
|
|
if (value in ChunkTokenNumMap) {
|
|
form.setFieldValue(
|
|
['parser_config', 'chunk_token_num'],
|
|
ChunkTokenNumMap[value as keyof typeof ChunkTokenNumMap],
|
|
);
|
|
}
|
|
},
|
|
[form],
|
|
);
|
|
|
|
return handleChange;
|
|
};
|