ragflow / web /src /hooks /logic-hooks.ts
balibabu
Fix: In order to distinguish the keys of a pair of messages, add a prefix to the id when rendering the message. #4409 (#4451)
2052ec7
raw
history blame
14.5 kB
import { Authorization } from '@/constants/authorization';
import { MessageType } from '@/constants/chat';
import { LanguageTranslationMap } from '@/constants/common';
import { ResponseType } from '@/interfaces/database/base';
import { IAnswer, Message } from '@/interfaces/database/chat';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { IClientConversation, IMessage } from '@/pages/chat/interface';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { buildMessageUuid } from '@/utils/chat';
import { PaginationProps, message } from 'antd';
import { FormInstance } from 'antd/lib';
import axios from 'axios';
import { EventSourceParserStream } from 'eventsource-parser/stream';
import { omit } from 'lodash';
import {
ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { useTranslate } from './common-hooks';
import { useSetPaginationParams } from './route-hook';
import { useFetchTenantInfo, useSaveSetting } from './user-setting-hooks';
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 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 timer = useRef<any>();
const resetAnswer = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
setAnswer({} as IAnswer);
clearTimeout(timer.current);
}, 1000);
}, []);
const send = useCallback(
async (
body: any,
controller?: AbortController,
): 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),
signal: controller?.signal,
});
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');
resetAnswer();
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);
resetAnswer();
return { data: await res, response };
} catch (e) {
setDone(true);
resetAnswer();
console.warn(e);
}
},
[url, resetAnswer],
);
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?.code !== 0) {
message.error(res?.message);
}
} 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), // The message id is generated on the front end,
// and the message id returned by the back end is the same as the question id,
// so that the pair of messages can be deleted together when deleting the 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,
...omit(answer, 'reference'),
},
];
});
}, []);
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) => x.id !== 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 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;
};
// reset form fields when modal is form, closed
export const useResetFormOnCloseModal = ({
form,
visible,
}: {
form: FormInstance;
visible?: boolean;
}) => {
const prevOpenRef = useRef<boolean>();
useEffect(() => {
prevOpenRef.current = visible;
}, [visible]);
const prevOpen = prevOpenRef.current;
useEffect(() => {
if (!visible && prevOpen) {
form.resetFields();
}
}, [form, prevOpen, visible]);
};