This commit is contained in:
2025-11-07 09:30:45 +08:00
parent ecdb4321e8
commit fa8ebffac9
1478 changed files with 188959 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
jest.mock('eventsource-parser/stream', () => ({}));
import { act, renderHook } from '@testing-library/react';
import { useScrollToBottom } from '../logic-hooks';
function createMockContainer({ atBottom = true } = {}) {
const scrollTop = atBottom ? 100 : 0;
const clientHeight = 100;
const scrollHeight = 200;
const listeners = {};
return {
current: {
scrollTop,
clientHeight,
scrollHeight,
addEventListener: jest.fn((event, cb) => {
listeners[event] = cb;
}),
removeEventListener: jest.fn(),
},
listeners,
} as any;
}
// Helper to flush all timers and microtasks
async function flushAll() {
jest.runAllTimers();
// Flush microtasks
await Promise.resolve();
// Sometimes, effects queue more timers, so run again
jest.runAllTimers();
await Promise.resolve();
}
describe('useScrollToBottom', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should set isAtBottom true when user is at bottom', () => {
const containerRef = createMockContainer({ atBottom: true });
const { result } = renderHook(() => useScrollToBottom([], containerRef));
expect(result.current.isAtBottom).toBe(true);
});
it('should set isAtBottom false when user is not at bottom', () => {
const containerRef = createMockContainer({ atBottom: false });
const { result } = renderHook(() => useScrollToBottom([], containerRef));
expect(result.current.isAtBottom).toBe(false);
});
it('should scroll to bottom when isAtBottom is true and messages change', async () => {
const containerRef = createMockContainer({ atBottom: true });
const mockScroll = jest.fn();
function useTestScrollToBottom(messages: any, containerRef: any) {
const hook = useScrollToBottom(messages, containerRef);
hook.scrollRef.current = { scrollIntoView: mockScroll } as any;
return hook;
}
const { rerender } = renderHook(
({ messages }) => useTestScrollToBottom(messages, containerRef),
{ initialProps: { messages: [] } },
);
rerender({ messages: ['msg1'] });
await flushAll();
expect(mockScroll).toHaveBeenCalled();
});
it('should NOT scroll to bottom when isAtBottom is false and messages change', async () => {
const containerRef = createMockContainer({ atBottom: false });
const mockScroll = jest.fn();
function useTestScrollToBottom(messages: any, containerRef: any) {
const hook = useScrollToBottom(messages, containerRef);
hook.scrollRef.current = { scrollIntoView: mockScroll } as any;
console.log('HOOK: isAtBottom:', hook.isAtBottom);
return hook;
}
const { result, rerender } = renderHook(
({ messages }) => useTestScrollToBottom(messages, containerRef),
{ initialProps: { messages: [] } },
);
// Simulate user scrolls up before messages change
await act(async () => {
containerRef.current.scrollTop = 0;
containerRef.current.addEventListener.mock.calls[0][1]();
await flushAll();
// Advance fake timers by 10ms instead of real setTimeout
jest.advanceTimersByTime(10);
console.log('AFTER SCROLL: isAtBottom:', result.current.isAtBottom);
});
rerender({ messages: ['msg1'] });
await flushAll();
console.log('AFTER RERENDER: isAtBottom:', result.current.isAtBottom);
expect(mockScroll).not.toHaveBeenCalled();
// Optionally, flush again after the assertion to see if it gets called late
await flushAll();
});
it('should indicate button should appear when user is not at bottom', () => {
const containerRef = createMockContainer({ atBottom: false });
const { result } = renderHook(() => useScrollToBottom([], containerRef));
// The button should appear in the UI when isAtBottom is false
expect(result.current.isAtBottom).toBe(false);
});
});
const originalRAF = global.requestAnimationFrame;
beforeAll(() => {
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
});
afterAll(() => {
global.requestAnimationFrame = originalRAF;
});

View File

@@ -0,0 +1,54 @@
import message from '@/components/ui/message';
import authorizationUtil from '@/utils/authorization-util';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'umi';
export const useOAuthCallback = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const error = currentQueryParameters.get('error');
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const navigate = useNavigate();
useEffect(() => {
if (error) {
message.error(error);
setTimeout(() => {
navigate('/login');
newQueryParameters.delete('error');
setSearchParams(newQueryParameters);
}, 1000);
return;
}
const auth = currentQueryParameters.get('auth');
if (auth) {
authorizationUtil.setAuthorization(auth);
newQueryParameters.delete('auth');
setSearchParams(newQueryParameters);
navigate('/');
}
}, [
error,
currentQueryParameters,
newQueryParameters,
navigate,
setSearchParams,
]);
console.debug(currentQueryParameters.get('auth'));
return currentQueryParameters.get('auth');
};
export const useAuth = () => {
const auth = useOAuthCallback();
const [isLogin, setIsLogin] = useState<Nullable<boolean>>(null);
useEffect(() => {
setIsLogin(!!authorizationUtil.getAuthorization() || !!auth);
}, [auth]);
return { isLogin };
};

642
web/src/hooks/chat-hooks.ts Normal file
View File

@@ -0,0 +1,642 @@
import { ChatSearchParams } from '@/constants/chat';
import {
IConversation,
IDialog,
IStats,
IToken,
} from '@/interfaces/database/chat';
import {
IAskRequestBody,
IFeedbackRequestBody,
} from '@/interfaces/request/chat';
import i18n from '@/locales/config';
import { IClientConversation } from '@/pages/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import chatService from '@/services/chat-service';
import {
buildMessageListWithUuid,
getConversationId,
isConversationIdExist,
} from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import { has, set } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { history, useSearchParams } from 'umi';
//#region logic
export const useClickDialogCard = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(() => {
return new URLSearchParams();
}, []);
const handleClickDialog = useCallback(
(dialogId: string) => {
newQueryParameters.set(ChatSearchParams.DialogId, dialogId);
// newQueryParameters.set(
// ChatSearchParams.ConversationId,
// EmptyConversationId,
// );
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
return { handleClickDialog };
};
export const useClickConversationCard = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const handleClickConversation = useCallback(
(conversationId: string, isNew: string) => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
newQueryParameters.set(ChatSearchParams.isNew, isNew);
setSearchParams(newQueryParameters);
},
[setSearchParams, newQueryParameters],
);
return { handleClickConversation };
};
export const useGetChatSearchParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
dialogId: currentQueryParameters.get(ChatSearchParams.DialogId) || '',
conversationId:
currentQueryParameters.get(ChatSearchParams.ConversationId) || '',
isNew: currentQueryParameters.get(ChatSearchParams.isNew) || '',
};
};
//#endregion
//#region dialog
export const useFetchNextDialogList = (pureFetch = false) => {
const { handleClickDialog } = useClickDialogCard();
const { dialogId } = useGetChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IDialog[]>({
queryKey: ['fetchDialogList'],
initialData: [],
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async (...params) => {
console.log('🚀 ~ queryFn: ~ params:', params);
const { data } = await chatService.listDialog();
if (data.code === 0) {
const list: IDialog[] = data.data;
if (!pureFetch) {
if (list.length > 0) {
if (list.every((x) => x.id !== dialogId)) {
handleClickDialog(data.data[0].id);
}
} else {
history.push('/chat');
}
}
}
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useFetchChatAppList = () => {
const {
data,
isFetching: loading,
refetch,
} = useQuery<IDialog[]>({
queryKey: ['fetchChatAppList'],
initialData: [],
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.listDialog();
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useSetNextDialog = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['setDialog'],
mutationFn: async (params: IDialog) => {
const { data } = await chatService.setDialog(params);
if (data.code === 0) {
queryClient.invalidateQueries({
exact: false,
queryKey: ['fetchDialogList'],
});
queryClient.invalidateQueries({
queryKey: ['fetchDialog'],
});
message.success(
i18n.t(`message.${params.dialog_id ? 'modified' : 'created'}`),
);
}
return data?.code;
},
});
return { data, loading, setDialog: mutateAsync };
};
export const useFetchNextDialog = () => {
const { dialogId } = useGetChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IDialog>({
queryKey: ['fetchDialog', dialogId],
gcTime: 0,
initialData: {} as IDialog,
enabled: !!dialogId,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.getDialog({ dialogId });
return data?.data ?? ({} as IDialog);
},
});
return { data, loading, refetch };
};
export const useFetchManualDialog = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchManualDialog'],
gcTime: 0,
mutationFn: async (dialogId: string) => {
const { data } = await chatService.getDialog({ dialogId });
return data;
},
});
return { data, loading, fetchDialog: mutateAsync };
};
export const useRemoveNextDialog = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['removeDialog'],
mutationFn: async (dialogIds: string[]) => {
const { data } = await chatService.removeDialog({ dialogIds });
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchDialogList'] });
message.success(i18n.t('message.deleted'));
}
return data.code;
},
});
return { data, loading, removeDialog: mutateAsync };
};
//#endregion
//#region conversation
export const useFetchNextConversationList = () => {
const { dialogId } = useGetChatSearchParams();
const { handleClickConversation } = useClickConversationCard();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IConversation[]>({
queryKey: ['fetchConversationList', dialogId],
initialData: [],
gcTime: 0,
refetchOnWindowFocus: false,
enabled: !!dialogId,
queryFn: async () => {
const { data } = await chatService.listConversation({ dialogId });
if (data.code === 0) {
if (data.data.length > 0) {
handleClickConversation(data.data[0].id, '');
} else {
handleClickConversation('', '');
}
}
return data?.data;
},
});
return { data, loading, refetch };
};
export const useFetchNextConversation = () => {
const { isNew, conversationId } = useGetChatSearchParams();
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IClientConversation>({
queryKey: ['fetchConversation', conversationId],
initialData: {} as IClientConversation,
// enabled: isConversationIdExist(conversationId),
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
if (
isNew !== 'true' &&
isConversationIdExist(sharedId || conversationId)
) {
const { data } = await chatService.getConversation({
conversationId: conversationId || sharedId,
});
const conversation = data?.data ?? {};
const messageList = buildMessageListWithUuid(conversation?.message);
return { ...conversation, message: messageList };
}
return { message: [] };
},
});
return { data, loading, refetch };
};
export const useFetchNextConversationSSE = () => {
const { isNew } = useGetChatSearchParams();
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IClientConversation>({
queryKey: ['fetchConversationSSE', sharedId],
initialData: {} as IClientConversation,
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
if (isNew !== 'true' && isConversationIdExist(sharedId || '')) {
if (!sharedId) return {};
const { data } = await chatService.getConversationSSE({}, sharedId);
const conversation = data?.data ?? {};
const messageList = buildMessageListWithUuid(conversation?.message);
return { ...conversation, message: messageList };
}
return { message: [] };
},
});
return { data, loading, refetch };
};
export const useFetchManualConversation = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchManualConversation'],
gcTime: 0,
mutationFn: async (conversationId: string) => {
const { data } = await chatService.getConversation({ conversationId });
return data;
},
});
return { data, loading, fetchConversation: mutateAsync };
};
export const useUpdateNextConversation = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['updateConversation'],
mutationFn: async (params: Record<string, any>) => {
const { data } = await chatService.setConversation({
...params,
conversation_id: params.conversation_id
? params.conversation_id
: getConversationId(),
});
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchConversationList'] });
message.success(i18n.t(`message.modified`));
}
return data;
},
});
return { data, loading, updateConversation: mutateAsync };
};
export const useRemoveNextConversation = () => {
const queryClient = useQueryClient();
const { dialogId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['removeConversation'],
mutationFn: async (conversationIds: string[]) => {
const { data } = await chatService.removeConversation({
conversationIds,
dialogId,
});
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchConversationList'] });
}
return data.code;
},
});
return { data, loading, removeConversation: mutateAsync };
};
export const useDeleteMessage = () => {
const { conversationId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteMessage'],
mutationFn: async (messageId: string) => {
const { data } = await chatService.deleteMessage({
messageId,
conversationId,
});
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
}
return data.code;
},
});
return { data, loading, deleteMessage: mutateAsync };
};
export const useFeedback = () => {
const { conversationId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['feedback'],
mutationFn: async (params: IFeedbackRequestBody) => {
const { data } = await chatService.thumbup({
...params,
conversationId,
});
if (data.code === 0) {
message.success(i18n.t(`message.operated`));
}
return data.code;
},
});
return { data, loading, feedback: mutateAsync };
};
//#endregion
// #region API provided for external calls
export const useCreateNextToken = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createToken'],
mutationFn: async (params: Record<string, any>) => {
const { data } = await chatService.createToken(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchTokenList'] });
}
return data?.data ?? [];
},
});
return { data, loading, createToken: mutateAsync };
};
export const useFetchTokenList = (params: Record<string, any>) => {
const {
data,
isFetching: loading,
refetch,
} = useQuery<IToken[]>({
queryKey: ['fetchTokenList', params],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await chatService.listToken(params);
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useRemoveNextToken = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['removeToken'],
mutationFn: async (params: {
tenantId: string;
dialogId?: string;
tokens: string[];
}) => {
const { data } = await chatService.removeToken(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchTokenList'] });
}
return data?.data ?? [];
},
});
return { data, loading, removeToken: mutateAsync };
};
type RangeValue = [Dayjs | null, Dayjs | null] | null;
const getDay = (date?: Dayjs) => date?.format('YYYY-MM-DD');
export const useFetchNextStats = () => {
const [pickerValue, setPickerValue] = useState<RangeValue>([
dayjs().subtract(7, 'day'),
dayjs(),
]);
const { data, isFetching: loading } = useQuery<IStats>({
queryKey: ['fetchStats', pickerValue],
initialData: {} as IStats,
gcTime: 0,
queryFn: async () => {
if (Array.isArray(pickerValue) && pickerValue[0]) {
const { data } = await chatService.getStats({
fromDate: getDay(pickerValue[0]),
toDate: getDay(pickerValue[1] ?? dayjs()),
});
return data?.data ?? {};
}
return {};
},
});
return { data, loading, pickerValue, setPickerValue };
};
//#endregion
//#region shared chat
export const useCreateNextSharedConversation = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createSharedConversation'],
mutationFn: async (userId?: string) => {
const { data } = await chatService.createExternalConversation({ userId });
return data;
},
});
return { data, loading, createSharedConversation: mutateAsync };
};
// deprecated
export const useFetchNextSharedConversation = (
conversationId?: string | null,
) => {
const { data, isPending: loading } = useQuery({
queryKey: ['fetchSharedConversation'],
enabled: !!conversationId,
queryFn: async () => {
if (!conversationId) {
return {};
}
const { data } = await chatService.getExternalConversation(
null,
conversationId,
);
const messageList = buildMessageListWithUuid(data?.data?.message);
set(data, 'data.message', messageList);
return data;
},
});
return { data, loading };
};
//#endregion
//#region search page
export const useFetchMindMap = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchMindMap'],
gcTime: 0,
mutationFn: async (params: IAskRequestBody) => {
try {
const ret = await chatService.getMindMap(params);
return ret?.data?.data ?? {};
} catch (error: any) {
if (has(error, 'message')) {
message.error(error.message);
}
return [];
}
},
});
return { data, loading, fetchMindMap: mutateAsync };
};
export const useFetchRelatedQuestions = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchRelatedQuestions'],
gcTime: 0,
mutationFn: async (question: string): Promise<string[]> => {
const { data } = await chatService.getRelatedQuestions({ question });
return data?.data ?? [];
},
});
return { data, loading, fetchRelatedQuestions: mutateAsync };
};
//#endregion

View File

@@ -0,0 +1,210 @@
import { ResponseGetType, ResponseType } from '@/interfaces/database/base';
import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { PaginationProps, message } from 'antd';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import {
useGetKnowledgeSearchParams,
useSetPaginationParams,
} from './route-hook';
export interface IChunkListResult {
searchString?: string;
handleInputChange?: React.ChangeEventHandler<HTMLInputElement>;
pagination: PaginationProps;
setPagination?: (pagination: { page: number; pageSize: number }) => void;
available: number | undefined;
handleSetAvailable: (available: number | undefined) => void;
}
export const useFetchNextChunkList = (): ResponseGetType<{
data: IChunk[];
total: number;
documentInfo: IKnowledgeFile;
}> &
IChunkListResult => {
const { pagination, setPagination } = useGetPaginationWithRouter();
const { documentId } = useGetKnowledgeSearchParams();
const { searchString, handleInputChange } = useHandleSearchChange();
const [available, setAvailable] = useState<number | undefined>();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { data, isFetching: loading } = useQuery({
queryKey: [
'fetchChunkList',
documentId,
pagination.current,
pagination.pageSize,
debouncedSearchString,
available,
],
placeholderData: (previousData) =>
previousData ?? { data: [], total: 0, documentInfo: {} }, // https://github.com/TanStack/query/issues/8183
gcTime: 0,
queryFn: async () => {
const { data } = await kbService.chunk_list({
doc_id: documentId,
page: pagination.current,
size: pagination.pageSize,
available_int: available,
keywords: searchString,
});
if (data.code === 0) {
const res = data.data;
return {
data: res.chunks,
total: res.total,
documentInfo: res.doc,
};
}
return (
data?.data ?? {
data: [],
total: 0,
documentInfo: {},
}
);
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);
const handleSetAvailable = useCallback(
(a: number | undefined) => {
setPagination({ page: 1 });
setAvailable(a);
},
[setAvailable, setPagination],
);
return {
data,
loading,
pagination,
setPagination,
searchString,
handleInputChange: onInputChange,
available,
handleSetAvailable,
};
};
export const useSelectChunkList = () => {
const queryClient = useQueryClient();
const data = queryClient.getQueriesData<{
data: IChunk[];
total: number;
documentInfo: IKnowledgeFile;
}>({ queryKey: ['fetchChunkList'] });
return data?.at(-1)?.[1];
};
export const useDeleteChunk = () => {
const queryClient = useQueryClient();
const { setPaginationParams } = useSetPaginationParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteChunk'],
mutationFn: async (params: { chunkIds: string[]; doc_id: string }) => {
const { data } = await kbService.rm_chunk(params);
if (data.code === 0) {
setPaginationParams(1);
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}
return data?.code;
},
});
return { data, loading, deleteChunk: mutateAsync };
};
export const useSwitchChunk = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['switchChunk'],
mutationFn: async (params: {
chunk_ids?: string[];
available_int?: number;
doc_id: string;
}) => {
const { data } = await kbService.switch_chunk(params);
if (data.code === 0) {
message.success(t('message.modified'));
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}
return data?.code;
},
});
return { data, loading, switchChunk: mutateAsync };
};
export const useCreateChunk = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createChunk'],
mutationFn: async (payload: any) => {
let service = kbService.create_chunk;
if (payload.chunk_id) {
service = kbService.set_chunk;
}
const { data } = await service(payload);
if (data.code === 0) {
message.success(t('message.created'));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}, 1000); // Delay to ensure the list is updated
}
return data?.code;
},
});
return { data, loading, createChunk: mutateAsync };
};
export const useFetchChunk = (chunkId?: string): ResponseType<any> => {
const { data } = useQuery({
queryKey: ['fetchChunk'],
enabled: !!chunkId,
initialData: {},
gcTime: 0,
queryFn: async () => {
const data = await kbService.get_chunk({
chunk_id: chunkId,
});
return data;
},
});
return data;
};

View File

@@ -0,0 +1,127 @@
import { ExclamationCircleFilled } from '@ant-design/icons';
import { App } from 'antd';
import isEqual from 'lodash/isEqual';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
export const useSetModalState = (initialVisible = false) => {
const [visible, setVisible] = useState(initialVisible);
const showModal = useCallback(() => {
setVisible(true);
}, []);
const hideModal = useCallback(() => {
setVisible(false);
}, []);
const switchVisible = useCallback(() => {
setVisible(!visible);
}, [visible]);
return { visible, showModal, hideModal, switchVisible };
};
export const useDeepCompareEffect = (
effect: React.EffectCallback,
deps: React.DependencyList,
) => {
const ref = useRef<React.DependencyList>();
let callback: ReturnType<React.EffectCallback> = () => {};
if (!isEqual(deps, ref.current)) {
callback = effect();
ref.current = deps;
}
useEffect(() => {
return () => {
if (callback) {
callback();
}
};
}, []);
};
export interface UseDynamicSVGImportOptions {
onCompleted?: (
name: string,
SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined,
) => void;
onError?: (err: Error) => void;
}
export function useDynamicSVGImport(
name: string,
options: UseDynamicSVGImportOptions = {},
) {
const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const { onCompleted, onError } = options;
useEffect(() => {
setLoading(true);
const importIcon = async (): Promise<void> => {
try {
ImportedIconRef.current = (await import(name)).ReactComponent;
onCompleted?.(name, ImportedIconRef.current);
} catch (err: any) {
onError?.(err);
setError(err);
} finally {
setLoading(false);
}
};
importIcon();
}, [name, onCompleted, onError]);
return { error, loading, SvgIcon: ImportedIconRef.current };
}
interface IProps {
title?: string;
content?: ReactNode;
onOk?: (...args: any[]) => any;
onCancel?: (...args: any[]) => any;
}
export const useShowDeleteConfirm = () => {
const { modal } = App.useApp();
const { t } = useTranslation();
const showDeleteConfirm = useCallback(
({ title, content, onOk, onCancel }: IProps): Promise<number> => {
return new Promise((resolve, reject) => {
modal.confirm({
title: title ?? t('common.deleteModalTitle'),
icon: <ExclamationCircleFilled />,
content,
okText: t('common.yes'),
okType: 'danger',
cancelText: t('common.no'),
async onOk() {
try {
const ret = await onOk?.();
resolve(ret);
console.info(ret);
} catch (error) {
reject(error);
}
},
onCancel() {
onCancel?.();
},
});
});
},
[t, modal],
);
return showDeleteConfirm;
};
export const useTranslate = (keyPrefix: string) => {
return useTranslation('translation', { keyPrefix });
};
export const useCommonTranslation = () => {
return useTranslation('translation', { keyPrefix: 'common' });
};

View File

@@ -0,0 +1,516 @@
import { IReferenceChunk } from '@/interfaces/database/chat';
import { IDocumentInfo } from '@/interfaces/database/document';
import { IChunk } from '@/interfaces/database/knowledge';
import {
IChangeParserConfigRequestBody,
IDocumentMetaRequestBody,
} from '@/interfaces/request/document';
import i18n from '@/locales/config';
import chatService from '@/services/chat-service';
import kbService, { listDocument } from '@/services/knowledge-service';
import api, { api_host } from '@/utils/api';
import { buildChunkHighlights } from '@/utils/document-util';
import { post } from '@/utils/request';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { UploadFile, message } from 'antd';
import { get } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { IHighlight } from 'react-pdf-highlighter';
import { useParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import {
useGetKnowledgeSearchParams,
useSetPaginationParams,
} from './route-hook';
export const useGetDocumentUrl = (documentId?: string) => {
const getDocumentUrl = useCallback(
(id?: string) => {
return `${api_host}/document/get/${documentId || id}`;
},
[documentId],
);
return getDocumentUrl;
};
export const useGetChunkHighlights = (
selectedChunk: IChunk | IReferenceChunk,
) => {
const [size, setSize] = useState({ width: 849, height: 1200 });
const highlights: IHighlight[] = useMemo(() => {
return buildChunkHighlights(selectedChunk, size);
}, [selectedChunk, size]);
const setWidthAndHeight = (width: number, height: number) => {
setSize((pre) => {
if (pre.height !== height || pre.width !== width) {
return { height, width };
}
return pre;
});
};
return { highlights, setWidthAndHeight };
};
export const useFetchNextDocumentList = () => {
const { knowledgeId } = useGetKnowledgeSearchParams();
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const { id } = useParams();
const { data, isFetching: loading } = useQuery<{
docs: IDocumentInfo[];
total: number;
}>({
queryKey: ['fetchDocumentList', searchString, pagination],
initialData: { docs: [], total: 0 },
refetchInterval: 15000,
enabled: !!knowledgeId || !!id,
queryFn: async () => {
const ret = await listDocument({
kb_id: knowledgeId || id,
keywords: searchString,
page_size: pagination.pageSize,
page: pagination.current,
});
if (ret.data.code === 0) {
return ret.data.data;
}
return {
docs: [],
total: 0,
};
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);
return {
loading,
searchString,
documents: data.docs,
pagination: { ...pagination, total: data?.total },
handleInputChange: onInputChange,
setPagination,
};
};
export const useSetNextDocumentStatus = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['updateDocumentStatus'],
mutationFn: async ({
status,
documentId,
}: {
status: boolean;
documentId: string | string[];
}) => {
const ids = Array.isArray(documentId) ? documentId : [documentId];
const { data } = await kbService.document_change_status({
doc_ids: ids,
status: Number(status),
});
if (data.code === 0) {
message.success(i18n.t('message.modified'));
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
}
return data;
},
});
return { setDocumentStatus: mutateAsync, data, loading };
};
export const useSaveNextDocumentName = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveDocumentName'],
mutationFn: async ({
name,
documentId,
}: {
name: string;
documentId: string;
}) => {
const { data } = await kbService.document_rename({
doc_id: documentId,
name: name,
});
if (data.code === 0) {
message.success(i18n.t('message.renamed'));
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
}
return data.code;
},
});
return { loading, saveName: mutateAsync, data };
};
export const useCreateNextDocument = () => {
const { knowledgeId } = useGetKnowledgeSearchParams();
const { setPaginationParams, page } = useSetPaginationParams();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createDocument'],
mutationFn: async (name: string) => {
const { data } = await kbService.document_create({
name,
kb_id: knowledgeId,
});
if (data.code === 0) {
if (page === 1) {
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
} else {
setPaginationParams(); // fetch document list
}
message.success(i18n.t('message.created'));
}
return data.code;
},
});
return { createDocument: mutateAsync, loading, data };
};
export const useSetNextDocumentParser = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['setDocumentParser'],
mutationFn: async ({
parserId,
documentId,
parserConfig,
}: {
parserId: string;
documentId: string;
parserConfig: IChangeParserConfigRequestBody;
}) => {
const { data } = await kbService.document_change_parser({
parser_id: parserId,
doc_id: documentId,
parser_config: parserConfig,
});
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
message.success(i18n.t('message.modified'));
}
return data.code;
},
});
return { setDocumentParser: mutateAsync, data, loading };
};
export const useUploadNextDocument = () => {
const queryClient = useQueryClient();
const { knowledgeId } = useGetKnowledgeSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['uploadDocument'],
mutationFn: async (fileList: UploadFile[]) => {
const formData = new FormData();
formData.append('kb_id', knowledgeId);
fileList.forEach((file: any) => {
formData.append('file', file);
});
try {
const ret = await kbService.document_upload(formData);
const code = get(ret, 'data.code');
if (code === 0 || code === 500) {
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
}
return ret?.data;
} catch (error) {
console.warn(error);
return {
code: 500,
message: error + '',
};
}
},
});
return { uploadDocument: mutateAsync, loading, data };
};
export const useNextWebCrawl = () => {
const { knowledgeId } = useGetKnowledgeSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['webCrawl'],
mutationFn: async ({ name, url }: { name: string; url: string }) => {
const formData = new FormData();
formData.append('name', name);
formData.append('url', url);
formData.append('kb_id', knowledgeId);
const ret = await kbService.web_crawl(formData);
const code = get(ret, 'data.code');
if (code === 0) {
message.success(i18n.t('message.uploaded'));
}
return code;
},
});
return {
data,
loading,
webCrawl: mutateAsync,
};
};
export const useRunNextDocument = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['runDocumentByIds'],
mutationFn: async ({
documentIds,
run,
shouldDelete,
}: {
documentIds: string[];
run: number;
shouldDelete: boolean;
}) => {
queryClient.invalidateQueries({
queryKey: ['fetchDocumentList'],
});
const ret = await kbService.document_run({
doc_ids: documentIds,
run,
delete: shouldDelete,
});
const code = get(ret, 'data.code');
if (code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
message.success(i18n.t('message.operated'));
}
return code;
},
});
return { runDocumentByIds: mutateAsync, loading, data };
};
export const useFetchDocumentInfosByIds = () => {
const [ids, setDocumentIds] = useState<string[]>([]);
const idList = useMemo(() => {
return ids.filter((x) => typeof x === 'string' && x !== '');
}, [ids]);
const { data } = useQuery<IDocumentInfo[]>({
queryKey: ['fetchDocumentInfos', idList],
enabled: idList.length > 0,
initialData: [],
queryFn: async () => {
const { data } = await kbService.document_infos({ doc_ids: idList });
if (data.code === 0) {
return data.data;
}
return [];
},
});
return { data, setDocumentIds };
};
export const useFetchDocumentThumbnailsByIds = () => {
const [ids, setDocumentIds] = useState<string[]>([]);
const { data } = useQuery<Record<string, string>>({
queryKey: ['fetchDocumentThumbnails', ids],
enabled: ids.length > 0,
initialData: {},
queryFn: async () => {
const { data } = await kbService.document_thumbnails({ doc_ids: ids });
if (data.code === 0) {
return data.data;
}
return {};
},
});
return { data, setDocumentIds };
};
export const useRemoveNextDocument = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['removeDocument'],
mutationFn: async (documentIds: string | string[]) => {
const { data } = await kbService.document_rm({ doc_id: documentIds });
if (data.code === 0) {
message.success(i18n.t('message.deleted'));
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
}
return data.code;
},
});
return { data, loading, removeDocument: mutateAsync };
};
export const useDeleteDocument = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteDocument'],
mutationFn: async (documentIds: string[]) => {
const data = await kbService.document_delete({ doc_ids: documentIds });
return data;
},
});
return { data, loading, deleteDocument: mutateAsync };
};
export const useUploadAndParseDocument = (uploadMethod: string) => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['uploadAndParseDocument'],
mutationFn: async ({
conversationId,
fileList,
}: {
conversationId: string;
fileList: UploadFile[];
}) => {
try {
const formData = new FormData();
formData.append('conversation_id', conversationId);
fileList.forEach((file: UploadFile) => {
formData.append('file', file as any);
});
if (uploadMethod === 'upload_and_parse') {
const data = await kbService.upload_and_parse(formData);
return data?.data;
}
const data = await chatService.uploadAndParseExternal(formData);
return data?.data;
} catch (error) {}
},
});
return { data, loading, uploadAndParseDocument: mutateAsync };
};
export const useParseDocument = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['parseDocument'],
mutationFn: async (url: string) => {
try {
const data = await post(api.parse, { url });
if (data?.code === 0) {
message.success(i18n.t('message.uploaded'));
}
return data;
} catch (error) {
message.error('error');
}
},
});
return { parseDocument: mutateAsync, data, loading };
};
export const useSetDocumentMeta = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['setDocumentMeta'],
mutationFn: async (params: IDocumentMetaRequestBody) => {
try {
const { data } = await kbService.setMeta({
meta: params.meta,
doc_id: params.documentId,
});
if (data?.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchDocumentList'] });
message.success(i18n.t('message.modified'));
}
return data?.code;
} catch (error) {
message.error('error');
}
},
});
return { setDocumentMeta: mutateAsync, data, loading };
};

View File

@@ -0,0 +1,293 @@
import message from '@/components/ui/message';
import { ResponseType } from '@/interfaces/database/base';
import { IFolder } from '@/interfaces/database/file-manager';
import { IConnectRequestBody } from '@/interfaces/request/file-manager';
import fileManagerService from '@/services/file-manager-service';
import { downloadFileFromBlob } from '@/utils/file-util';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { PaginationProps, UploadFile } from 'antd';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { useSetPaginationParams } from './route-hook';
export const useGetFolderId = () => {
const [searchParams] = useSearchParams();
const id = searchParams.get('folderId') as string;
return id ?? '';
};
export interface IListResult {
searchString: string;
handleInputChange: React.ChangeEventHandler<HTMLInputElement>;
pagination: PaginationProps;
setPagination: (pagination: { page: number; pageSize: number }) => void;
loading: boolean;
}
export const useFetchPureFileList = () => {
const { mutateAsync, isPending: loading } = useMutation({
mutationKey: ['fetchPureFileList'],
gcTime: 0,
mutationFn: async (parentId: string) => {
const { data } = await fileManagerService.listFile({
parent_id: parentId,
});
return data;
},
});
return { loading, fetchList: mutateAsync };
};
export const useFetchFileList = (): ResponseType<any> & IListResult => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const id = useGetFolderId();
const { data, isFetching: loading } = useQuery({
queryKey: [
'fetchFileList',
{
id,
searchString,
...pagination,
},
],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await fileManagerService.listFile({
parent_id: id,
keywords: searchString,
page_size: pagination.pageSize,
page: pagination.current,
});
return data;
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);
return {
...data,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.data?.total },
setPagination,
loading,
};
};
export const useDeleteFile = () => {
const { setPaginationParams } = useSetPaginationParams();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteFile'],
mutationFn: async (params: { fileIds: string[]; parentId: string }) => {
const { data } = await fileManagerService.removeFile(params);
if (data.code === 0) {
setPaginationParams(1); // TODO: There should be a better way to paginate the request list
queryClient.invalidateQueries({ queryKey: ['fetchFileList'] });
}
return data.code;
},
});
return { data, loading, deleteFile: mutateAsync };
};
export const useDownloadFile = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['downloadFile'],
mutationFn: async (params: { id: string; filename?: string }) => {
const response = await fileManagerService.getFile({}, params.id);
const blob = new Blob([response.data], { type: response.data.type });
downloadFileFromBlob(blob, params.filename);
},
});
return { data, loading, downloadFile: mutateAsync };
};
export const useRenameFile = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['renameFile'],
mutationFn: async (params: { fileId: string; name: string }) => {
const { data } = await fileManagerService.renameFile(params);
if (data.code === 0) {
message.success(t('message.renamed'));
queryClient.invalidateQueries({ queryKey: ['fetchFileList'] });
}
return data.code;
},
});
return { data, loading, renameFile: mutateAsync };
};
export const useFetchParentFolderList = (): IFolder[] => {
const id = useGetFolderId();
const { data } = useQuery({
queryKey: ['fetchParentFolderList', id],
initialData: [],
enabled: !!id,
queryFn: async () => {
const { data } = await fileManagerService.getAllParentFolder({
fileId: id,
});
return data?.data?.parent_folders?.toReversed() ?? [];
},
});
return data;
};
export const useCreateFolder = () => {
const { setPaginationParams } = useSetPaginationParams();
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createFolder'],
mutationFn: async (params: { parentId: string; name: string }) => {
const { data } = await fileManagerService.createFolder({
...params,
type: 'folder',
});
if (data.code === 0) {
message.success(t('message.created'));
setPaginationParams(1);
queryClient.invalidateQueries({ queryKey: ['fetchFileList'] });
}
return data.code;
},
});
return { data, loading, createFolder: mutateAsync };
};
export const useUploadFile = () => {
const { setPaginationParams } = useSetPaginationParams();
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['uploadFile'],
mutationFn: async (params: {
fileList: UploadFile[];
parentId: string;
}) => {
const fileList = params.fileList;
const pathList = params.fileList.map(
(file) => (file as any).webkitRelativePath,
);
const formData = new FormData();
formData.append('parent_id', params.parentId);
fileList.forEach((file: any, index: number) => {
formData.append('file', file);
formData.append('path', pathList[index]);
});
try {
const ret = await fileManagerService.uploadFile(formData);
if (ret?.data.code === 0) {
message.success(t('message.uploaded'));
setPaginationParams(1);
queryClient.invalidateQueries({ queryKey: ['fetchFileList'] });
}
return ret?.data?.code;
} catch (error) {
console.log('🚀 ~ useUploadFile ~ error:', error);
}
},
});
return { data, loading, uploadFile: mutateAsync };
};
export const useConnectToKnowledge = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['connectFileToKnowledge'],
mutationFn: async (params: IConnectRequestBody) => {
const { data } = await fileManagerService.connectFileToKnowledge(params);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({ queryKey: ['fetchFileList'] });
}
return data.code;
},
});
return { data, loading, connectFileToKnowledge: mutateAsync };
};
export interface IMoveFileBody {
src_file_ids: string[];
dest_file_id: string; // target folder id
}
export const useMoveFile = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['moveFile'],
mutationFn: async (params: IMoveFileBody) => {
const { data } = await fileManagerService.moveFile(params);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({ queryKey: ['fetchFileList'] });
}
return data.code;
},
});
return { data, loading, moveFile: mutateAsync };
};

316
web/src/hooks/flow-hooks.ts Normal file
View File

@@ -0,0 +1,316 @@
import { DSL, IFlow } from '@/interfaces/database/flow';
import { IDebugSingleRequestBody } from '@/interfaces/request/flow';
import i18n from '@/locales/config';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import flowService from '@/services/flow-service';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { set } from 'lodash';
import get from 'lodash/get';
import { useParams } from 'umi';
export const useFetchFlowList = (): { data: IFlow[]; loading: boolean } => {
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchFlowList'],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await flowService.listCanvas();
return data?.data ?? [];
},
});
return { data, loading };
};
export const useFetchListVersion = (
canvas_id: string,
): {
data: {
created_at: string;
title: string;
id: string;
}[];
loading: boolean;
} => {
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchListVersion'],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await flowService.getListVersion({}, canvas_id);
return data?.data ?? [];
},
});
return { data, loading };
};
export const useFetchVersion = (
version_id?: string,
): {
data?: IFlow;
loading: boolean;
} => {
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchVersion', version_id],
initialData: undefined,
gcTime: 0,
enabled: !!version_id, // Only call API when both values are provided
queryFn: async () => {
if (!version_id) return undefined;
const { data } = await flowService.getVersion({}, version_id);
return data?.data ?? undefined;
},
});
return { data, loading };
};
export const useFetchFlow = (): {
data: IFlow;
loading: boolean;
refetch: () => void;
} => {
const { id } = useParams();
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery({
queryKey: ['flowDetail'],
initialData: {} as IFlow,
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
queryFn: async () => {
const { data } = await flowService.getCanvas({}, sharedId || id);
const messageList = buildMessageListWithUuid(
get(data, 'data.dsl.messages', []),
);
set(data, 'data.dsl.messages', messageList);
return data?.data ?? {};
},
});
return { data, loading, refetch };
};
export const useSettingFlow = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['SettingFlow'],
mutationFn: async (params: any) => {
const ret = await flowService.settingCanvas(params);
if (ret?.data?.code === 0) {
message.success('success');
} else {
message.error(ret?.data?.data);
}
return ret;
},
});
return { data, loading, settingFlow: mutateAsync };
};
export const useFetchFlowSSE = (): {
data: IFlow;
loading: boolean;
refetch: () => void;
} => {
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery({
queryKey: ['flowDetailSSE'],
initialData: {} as IFlow,
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
queryFn: async () => {
if (!sharedId) return {};
const { data } = await flowService.getCanvasSSE({}, sharedId);
const messageList = buildMessageListWithUuid(
get(data, 'data.dsl.messages', []),
);
set(data, 'data.dsl.messages', messageList);
return data?.data ?? {};
},
});
return { data, loading, refetch };
};
export const useSetFlow = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['setFlow'],
mutationFn: async (params: {
id?: string;
title?: string;
dsl?: DSL;
avatar?: string;
}) => {
const { data = {} } = await flowService.setCanvas(params);
if (data.code === 0) {
message.success(
i18n.t(`message.${params?.id ? 'modified' : 'created'}`),
);
queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] });
}
return data;
},
});
return { data, loading, setFlow: mutateAsync };
};
export const useDeleteFlow = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteFlow'],
mutationFn: async (canvasIds: string[]) => {
const { data } = await flowService.removeCanvas({ canvasIds });
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: ['infiniteFetchFlowListTeam'],
});
}
return data?.data ?? [];
},
});
return { data, loading, deleteFlow: mutateAsync };
};
export const useRunFlow = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['runFlow'],
mutationFn: async (params: { id: string; dsl: DSL }) => {
const { data } = await flowService.runCanvas(params);
if (data.code === 0) {
message.success(i18n.t(`message.modified`));
}
return data?.data ?? {};
},
});
return { data, loading, runFlow: mutateAsync };
};
export const useResetFlow = () => {
const { id } = useParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['resetFlow'],
mutationFn: async () => {
const { data } = await flowService.resetCanvas({ id });
return data;
},
});
return { data, loading, resetFlow: mutateAsync };
};
export const useTestDbConnect = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testDbConnect'],
mutationFn: async (params: any) => {
const ret = await flowService.testDbConnect(params);
if (ret?.data?.code === 0) {
message.success(ret?.data?.data);
} else {
message.error(ret?.data?.data);
}
return ret;
},
});
return { data, loading, testDbConnect: mutateAsync };
};
export const useFetchInputElements = (componentId?: string) => {
const { id } = useParams();
const { data, isPending: loading } = useQuery({
queryKey: ['fetchInputElements', id, componentId],
initialData: [],
enabled: !!id && !!componentId,
retryOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
gcTime: 0,
queryFn: async () => {
try {
const { data } = await flowService.getInputElements({
id,
component_id: componentId,
});
return data?.data ?? [];
} catch (error) {
console.log('🚀 ~ queryFn: ~ error:', error);
}
},
});
return { data, loading };
};
export const useDebugSingle = () => {
const { id } = useParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['debugSingle'],
mutationFn: async (params: IDebugSingleRequestBody) => {
const ret = await flowService.debugSingle({ id, ...params });
if (ret?.data?.code !== 0) {
message.error(ret?.data?.message);
}
return ret?.data?.data;
},
});
return { data, loading, debugSingle: mutateAsync };
};

View File

@@ -0,0 +1,498 @@
import { ResponsePostType } from '@/interfaces/database/base';
import {
IKnowledge,
IKnowledgeGraph,
IRenameTag,
ITestingResult,
} from '@/interfaces/database/knowledge';
import i18n from '@/locales/config';
import kbService, {
deleteKnowledgeGraph,
getKnowledgeGraph,
listDataset,
listTag,
removeTag,
renameTag,
} from '@/services/knowledge-service';
import {
useInfiniteQuery,
useIsMutating,
useMutation,
useMutationState,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { message } from 'antd';
import { useState } from 'react';
import { useParams, useSearchParams } from 'umi';
import { useHandleSearchChange } from './logic-hooks';
import { useSetPaginationParams } from './route-hook';
export const useKnowledgeBaseId = (): string => {
const [searchParams] = useSearchParams();
const { id } = useParams();
const knowledgeBaseId = searchParams.get('id') || id;
return knowledgeBaseId || '';
};
export const useFetchKnowledgeBaseConfiguration = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const { data, isFetching: loading } = useQuery<IKnowledge>({
queryKey: ['fetchKnowledgeDetail'],
initialData: {} as IKnowledge,
gcTime: 0,
queryFn: async () => {
const { data } = await kbService.get_kb_detail({
kb_id: knowledgeBaseId,
});
return data?.data ?? {};
},
});
return { data, loading };
};
export const useFetchKnowledgeList = (
shouldFilterListWithoutDocument: boolean = false,
): {
list: IKnowledge[];
loading: boolean;
} => {
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchKnowledgeList'],
initialData: [],
gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
queryFn: async () => {
const { data } = await listDataset();
const list = data?.data?.kbs ?? [];
return shouldFilterListWithoutDocument
? list.filter((x: IKnowledge) => x.chunk_num > 0)
: list;
},
});
return { list: data, loading };
};
export const useSelectKnowledgeOptions = () => {
const { list } = useFetchKnowledgeList();
const options = list?.map((item) => ({
label: item.name,
value: item.id,
}));
return options;
};
export const useInfiniteFetchKnowledgeList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const PageSize = 30;
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['infiniteFetchKnowledgeList', debouncedSearchString],
queryFn: async ({ pageParam }) => {
const { data } = await listDataset({
page: pageParam,
page_size: PageSize,
keywords: debouncedSearchString,
});
const list = data?.data ?? [];
return list;
},
initialPageParam: 1,
getNextPageParam: (lastPage, pages, lastPageParam) => {
if (lastPageParam * PageSize <= lastPage.total) {
return lastPageParam + 1;
}
return undefined;
},
});
return {
data,
loading: isFetching,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
handleInputChange,
searchString,
};
};
export const useCreateKnowledge = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['infiniteFetchKnowledgeList'],
mutationFn: async (params: { id?: string; name: string }) => {
const { data = {} } = await kbService.createKb(params);
if (data.code === 0) {
message.success(
i18n.t(`message.${params?.id ? 'modified' : 'created'}`),
);
queryClient.invalidateQueries({ queryKey: ['fetchKnowledgeList'] });
}
return data;
},
});
return { data, loading, createKnowledge: mutateAsync };
};
export const useDeleteKnowledge = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteKnowledge'],
mutationFn: async (id: string) => {
const { data } = await kbService.rmKb({ kb_id: id });
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: ['infiniteFetchKnowledgeList'],
});
}
return data?.data ?? [];
},
});
return { data, loading, deleteKnowledge: mutateAsync };
};
//#region knowledge configuration
export const useUpdateKnowledge = (shouldFetchList = false) => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveKnowledge'],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await kbService.updateKb({
kb_id: params?.kb_id ? params?.kb_id : knowledgeBaseId,
...params,
});
if (data.code === 0) {
message.success(i18n.t(`message.updated`));
if (shouldFetchList) {
queryClient.invalidateQueries({
queryKey: ['fetchKnowledgeListByPage'],
});
} else {
queryClient.invalidateQueries({ queryKey: ['fetchKnowledgeDetail'] });
}
}
return data;
},
});
return { data, loading, saveKnowledgeConfiguration: mutateAsync };
};
//#endregion
//#region Retrieval testing
export const useTestChunkRetrieval = (): ResponsePostType<ITestingResult> & {
testChunk: (...params: any[]) => void;
} => {
const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testChunk'], // This method is invalid
gcTime: 0,
mutationFn: async (values: any) => {
const { data } = await kbService.retrieval_test({
...values,
kb_id: values.kb_id ?? knowledgeBaseId,
page,
size: pageSize,
});
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 = (): ResponsePostType<ITestingResult> & {
testChunkAll: (...params: any[]) => void;
} => {
const knowledgeBaseId = useKnowledgeBaseId();
const { page, size: pageSize } = useSetPaginationParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['testChunkAll'], // This method is invalid
gcTime: 0,
mutationFn: async (values: any) => {
const { data } = await kbService.retrieval_test({
...values,
kb_id: values.kb_id ?? knowledgeBaseId,
doc_ids: [],
page,
size: pageSize,
});
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 useChunkIsTesting = () => {
return useIsMutating({ mutationKey: ['testChunk'] }) > 0;
};
export const useSelectTestingResult = (): ITestingResult => {
const data = useMutationState({
filters: { mutationKey: ['testChunk'] },
select: (mutation) => {
return mutation.state.data;
},
});
return (data.at(-1) ?? {
chunks: [],
documents: [],
total: 0,
}) as ITestingResult;
};
export const useSelectIsTestingSuccess = () => {
const status = useMutationState({
filters: { mutationKey: ['testChunk'] },
select: (mutation) => {
return mutation.state.status;
},
});
return status.at(-1) === 'success';
};
export const useAllTestingSuccess = () => {
const status = useMutationState({
filters: { mutationKey: ['testChunkAll'] },
select: (mutation) => {
return mutation.state.status;
},
});
return status.at(-1) === 'success';
};
export const useAllTestingResult = (): ITestingResult => {
const data = useMutationState({
filters: { mutationKey: ['testChunkAll'] },
select: (mutation) => {
return mutation.state.data;
},
});
return (data.at(-1) ?? {
chunks: [],
documents: [],
total: 0,
}) as ITestingResult;
};
//#endregion
//#region tags
export const useFetchTagList = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const { data, isFetching: loading } = useQuery<Array<[string, number]>>({
queryKey: ['fetchTagList'],
initialData: [],
gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
queryFn: async () => {
const { data } = await listTag(knowledgeBaseId);
const list = data?.data || [];
return list;
},
});
return { list: data, loading };
};
export const useDeleteTag = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteTag'],
mutationFn: async (tags: string[]) => {
const { data } = await removeTag(knowledgeBaseId, tags);
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: ['fetchTagList'],
});
}
return data?.data ?? [];
},
});
return { data, loading, deleteTag: mutateAsync };
};
export const useRenameTag = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['renameTag'],
mutationFn: async (params: IRenameTag) => {
const { data } = await renameTag(knowledgeBaseId, params);
if (data.code === 0) {
message.success(i18n.t(`message.modified`));
queryClient.invalidateQueries({
queryKey: ['fetchTagList'],
});
}
return data?.data ?? [];
},
});
return { data, loading, renameTag: mutateAsync };
};
export const useTagIsRenaming = () => {
return useIsMutating({ mutationKey: ['renameTag'] }) > 0;
};
export const useFetchTagListByKnowledgeIds = () => {
const [knowledgeIds, setKnowledgeIds] = useState<string[]>([]);
const { data, isFetching: loading } = useQuery<Array<[string, number]>>({
queryKey: ['fetchTagListByKnowledgeIds'],
enabled: knowledgeIds.length > 0,
initialData: [],
gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
queryFn: async () => {
const { data } = await kbService.listTagByKnowledgeIds({
kb_ids: knowledgeIds.join(','),
});
const list = data?.data || [];
return list;
},
});
return { list: data, loading, setKnowledgeIds };
};
//#endregion
export function useFetchKnowledgeGraph() {
const knowledgeBaseId = useKnowledgeBaseId();
const { data, isFetching: loading } = useQuery<IKnowledgeGraph>({
queryKey: ['fetchKnowledgeGraph', knowledgeBaseId],
initialData: { graph: {}, mind_map: {} } as IKnowledgeGraph,
enabled: !!knowledgeBaseId,
gcTime: 0,
queryFn: async () => {
const { data } = await getKnowledgeGraph(knowledgeBaseId);
return data?.data;
},
});
return { data, loading };
}
export const useRemoveKnowledgeGraph = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['removeKnowledgeGraph'],
mutationFn: async () => {
const { data } = await deleteKnowledgeGraph(knowledgeBaseId);
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: ['fetchKnowledgeGraph'],
});
}
return data?.code;
},
});
return { data, loading, removeKnowledgeGraph: mutateAsync };
};

384
web/src/hooks/llm-hooks.tsx Normal file
View File

@@ -0,0 +1,384 @@
import { LlmIcon } from '@/components/svg-icon';
import { LlmModelType } from '@/constants/knowledge';
import { ResponseGetType } from '@/interfaces/database/base';
import {
IFactory,
IMyLlmValue,
IThirdOAIModelCollection as IThirdAiModelCollection,
IThirdOAIModel,
IThirdOAIModelCollection,
} from '@/interfaces/database/llm';
import {
IAddLlmRequestBody,
IDeleteLlmRequestBody,
} from '@/interfaces/request/llm';
import userService from '@/services/user-service';
import { sortLLmFactoryListBySpecifiedOrder } from '@/utils/common-util';
import { getLLMIconName, getRealModelName } from '@/utils/llm-util';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Flex, message } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useFetchLlmList = (
modelType?: LlmModelType,
): IThirdAiModelCollection => {
const { data } = useQuery({
queryKey: ['llmList'],
initialData: {},
queryFn: async () => {
const { data } = await userService.llm_list({ model_type: modelType });
return data?.data ?? {};
},
});
return data;
};
export const useSelectLlmOptions = () => {
const llmInfo: IThirdOAIModelCollection = useFetchLlmList();
const embeddingModelOptions = useMemo(() => {
return Object.entries(llmInfo).map(([key, value]) => {
return {
label: key,
options: value.map((x) => ({
label: getRealModelName(x.llm_name),
value: `${x.llm_name}@${x.fid}`,
disabled: !x.available,
})),
};
});
}, [llmInfo]);
return embeddingModelOptions;
};
function buildLlmOptionsWithIcon(x: IThirdOAIModel) {
return {
label: (
<Flex align="center" gap={6}>
<LlmIcon
name={getLLMIconName(x.fid, x.llm_name)}
width={26}
height={26}
size={'small'}
/>
<span>{getRealModelName(x.llm_name)}</span>
</Flex>
),
value: `${x.llm_name}@${x.fid}`,
disabled: !x.available,
is_tools: x.is_tools,
};
}
export const useSelectLlmOptionsByModelType = () => {
const llmInfo: IThirdOAIModelCollection = useFetchLlmList();
const groupImage2TextOptions = useCallback(() => {
const modelType = LlmModelType.Image2text;
const modelTag = modelType.toUpperCase();
return Object.entries(llmInfo)
.map(([key, value]) => {
return {
label: key,
options: value
.filter(
(x) =>
(x.model_type.includes(modelType) ||
(x.tags && x.tags.includes(modelTag))) &&
x.available,
)
.map(buildLlmOptionsWithIcon),
};
})
.filter((x) => x.options.length > 0);
}, [llmInfo]);
const groupOptionsByModelType = useCallback(
(modelType: LlmModelType) => {
return Object.entries(llmInfo)
.filter(([, value]) =>
modelType
? value.some((x) => x.model_type.includes(modelType))
: true,
)
.map(([key, value]) => {
return {
label: key,
options: value
.filter(
(x) =>
(modelType ? x.model_type.includes(modelType) : true) &&
x.available,
)
.map(buildLlmOptionsWithIcon),
};
})
.filter((x) => x.options.length > 0);
},
[llmInfo],
);
return {
[LlmModelType.Chat]: groupOptionsByModelType(LlmModelType.Chat),
[LlmModelType.Embedding]: groupOptionsByModelType(LlmModelType.Embedding),
[LlmModelType.Image2text]: groupImage2TextOptions(),
[LlmModelType.Speech2text]: groupOptionsByModelType(
LlmModelType.Speech2text,
),
[LlmModelType.Rerank]: groupOptionsByModelType(LlmModelType.Rerank),
[LlmModelType.TTS]: groupOptionsByModelType(LlmModelType.TTS),
};
};
// Merge different types of models from the same manufacturer under one manufacturer
export const useComposeLlmOptionsByModelTypes = (
modelTypes: LlmModelType[],
) => {
const allOptions = useSelectLlmOptionsByModelType();
return modelTypes.reduce<
(DefaultOptionType & {
options: {
label: JSX.Element;
value: string;
disabled: boolean;
is_tools: boolean;
}[];
})[]
>((pre, cur) => {
const options = allOptions[cur];
options.forEach((x) => {
const item = pre.find((y) => y.label === x.label);
if (item) {
x.options.forEach((y) => {
// A model that is both an image2text and speech2text model
if (!item.options.some((z) => z.value === y.value)) {
item.options.push(y);
}
});
} else {
pre.push(x);
}
});
return pre;
}, []);
};
export const useFetchLlmFactoryList = (): ResponseGetType<IFactory[]> => {
const { data, isFetching: loading } = useQuery({
queryKey: ['factoryList'],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await userService.factories_list();
return data?.data ?? [];
},
});
return { data, loading };
};
export type LlmItem = { name: string; logo: string } & IMyLlmValue;
export const useFetchMyLlmList = (): ResponseGetType<
Record<string, IMyLlmValue>
> => {
const { data, isFetching: loading } = useQuery({
queryKey: ['myLlmList'],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.my_llm();
return data?.data ?? {};
},
});
return { data, loading };
};
export const useFetchMyLlmListDetailed = (): ResponseGetType<
Record<string, any>
> => {
const { data, isFetching: loading } = useQuery({
queryKey: ['myLlmListDetailed'],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.my_llm({ include_details: true });
return data?.data ?? {};
},
});
return { data, loading };
};
export const useSelectLlmList = () => {
const { data: myLlmList, loading: myLlmListLoading } = useFetchMyLlmList();
const { data: factoryList, loading: factoryListLoading } =
useFetchLlmFactoryList();
const nextMyLlmList: Array<LlmItem> = useMemo(() => {
return Object.entries(myLlmList).map(([key, value]) => ({
name: key,
logo: factoryList.find((x) => x.name === key)?.logo ?? '',
...value,
llm: value.llm.map((x) => ({ ...x, name: x.name })),
}));
}, [myLlmList, factoryList]);
const nextFactoryList = useMemo(() => {
const currentList = factoryList.filter((x) =>
Object.keys(myLlmList).every((y) => y !== x.name),
);
return sortLLmFactoryListBySpecifiedOrder(currentList);
}, [factoryList, myLlmList]);
return {
myLlmList: nextMyLlmList,
factoryList: nextFactoryList,
loading: myLlmListLoading || factoryListLoading,
};
};
export interface IApiKeySavingParams {
llm_factory: string;
api_key: string;
llm_name?: string;
model_type?: string;
base_url?: string;
}
export const useSaveApiKey = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveApiKey'],
mutationFn: async (params: IApiKeySavingParams) => {
const { data } = await userService.set_api_key(params);
if (data.code === 0) {
message.success(t('message.modified'));
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
}
return data.code;
},
});
return { data, loading, saveApiKey: mutateAsync };
};
export interface ISystemModelSettingSavingParams {
tenant_id: string;
name?: string;
asr_id: string;
embd_id: string;
img2txt_id: string;
llm_id: string;
}
export const useSaveTenantInfo = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveTenantInfo'],
mutationFn: async (params: ISystemModelSettingSavingParams) => {
const { data } = await userService.set_tenant_info(params);
if (data.code === 0) {
message.success(t('message.modified'));
}
return data.code;
},
});
return { data, loading, saveTenantInfo: mutateAsync };
};
export const useAddLlm = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['addLlm'],
mutationFn: async (params: IAddLlmRequestBody) => {
const { data } = await userService.add_llm(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
message.success(t('message.modified'));
}
return data.code;
},
});
return { data, loading, addLlm: mutateAsync };
};
export const useDeleteLlm = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteLlm'],
mutationFn: async (params: IDeleteLlmRequestBody) => {
const { data } = await userService.delete_llm(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
message.success(t('message.deleted'));
}
return data.code;
},
});
return { data, loading, deleteLlm: mutateAsync };
};
export const useDeleteFactory = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteFactory'],
mutationFn: async (params: IDeleteLlmRequestBody) => {
const { data } = await userService.deleteFactory(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
message.success(t('message.deleted'));
}
return data.code;
},
});
return { data, loading, deleteFactory: mutateAsync };
};

View File

@@ -0,0 +1,748 @@
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 { has, isEmpty, 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 function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export const useSetSelectedRecord = <T = IKnowledgeFile>() => {
const [currentRecord, setCurrentRecord] = useState<T>({} as T);
const setRecord = (record: T) => {
setCurrentRecord(record);
};
return { currentRecord, setRecord };
};
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 }) => {
if (pagination.pageSize !== pageSize) {
pagination.page = 1; // Reset to first page if pageSize changes
}
setPaginationParams(pagination.page, pagination.pageSize);
},
[setPaginationParams, pageSize],
);
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 useHandleSearchChange = () => {
const [searchString, setSearchString] = useState('');
const { setPagination } = useGetPaginationWithRouter();
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value;
setSearchString(value);
setPagination({ page: 1 });
},
[setPagination],
);
return { handleInputChange, searchString };
};
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;
};
function useSetDoneRecord() {
const [doneRecord, setDoneRecord] = useState<Record<string, boolean>>({});
const clearDoneRecord = useCallback(() => {
setDoneRecord({});
}, []);
const setDoneRecordById = useCallback((id: string, val: boolean) => {
setDoneRecord((prev) => ({ ...prev, [id]: val }));
}, []);
const allDone = useMemo(() => {
return Object.values(doneRecord).every((val) => val);
}, [doneRecord]);
useEffect(() => {
if (!isEmpty(doneRecord) && allDone) {
clearDoneRecord();
}
}, [allDone, clearDoneRecord, doneRecord]);
return {
doneRecord,
setDoneRecord,
setDoneRecordById,
clearDoneRecord,
allDone,
};
}
export const useSendMessageWithSse = (
url: string = api.completeConversation,
) => {
const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
const [done, setDone] = useState(true);
const { doneRecord, clearDoneRecord, setDoneRecordById, allDone } =
useSetDoneRecord();
const timer = useRef<any>();
const sseRef = useRef<AbortController>();
const initializeSseRef = useCallback(() => {
sseRef.current = new AbortController();
}, []);
const resetAnswer = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
setAnswer({} as IAnswer);
clearTimeout(timer.current);
}, 1000);
}, []);
const setDoneValue = useCallback(
(body: any, value: boolean) => {
if (has(body, 'chatBoxId')) {
setDoneRecordById(body.chatBoxId, value);
} else {
setDone(value);
}
},
[setDoneRecordById],
);
const send = useCallback(
async (
body: any,
controller?: AbortController,
): Promise<{ response: Response; data: ResponseType } | undefined> => {
initializeSseRef();
try {
setDoneValue(body, false);
const response = await fetch(url, {
method: 'POST',
headers: {
[Authorization]: getAuthorization(),
'Content-Type': 'application/json',
},
body: JSON.stringify(omit(body, 'chatBoxId')),
signal: controller?.signal || sseRef.current?.signal,
});
const res = response.clone().json();
const reader = response?.body
?.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
.getReader();
while (true) {
try {
const x = await reader?.read();
if (x) {
const { done, value } = x;
if (done) {
resetAnswer();
break;
}
try {
const val = JSON.parse(value?.data || '');
const d = val?.data;
if (typeof d !== 'boolean') {
setAnswer({
...d,
conversationId: body?.conversation_id,
chatBoxId: body.chatBoxId,
});
}
} catch (e) {
// Swallow parse errors silently
}
}
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.log('Request was aborted by user or logic.');
break;
}
}
}
setDoneValue(body, true);
resetAnswer();
return { data: await res, response };
} catch (e) {
setDoneValue(body, true);
resetAnswer();
// Swallow fetch errors silently
}
},
[initializeSseRef, setDoneValue, url, resetAnswer],
);
const stopOutputMessage = useCallback(() => {
sseRef.current?.abort();
}, []);
return {
send,
answer,
done,
doneRecord,
allDone,
setDone,
resetAnswer,
stopOutputMessage,
clearDoneRecord,
};
};
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) {
// Swallow errors silently
}
return response;
},
[url],
);
return { read };
};
//#region chat hooks
export const useScrollToBottom = (
messages?: unknown,
containerRef?: React.RefObject<HTMLDivElement>,
) => {
const ref = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const isAtBottomRef = useRef(true);
useEffect(() => {
isAtBottomRef.current = isAtBottom;
}, [isAtBottom]);
const checkIfUserAtBottom = useCallback(() => {
if (!containerRef?.current) return true;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
return Math.abs(scrollTop + clientHeight - scrollHeight) < 25;
}, [containerRef]);
useEffect(() => {
if (!containerRef?.current) return;
const container = containerRef.current;
const handleScroll = () => {
setIsAtBottom(checkIfUserAtBottom());
};
container.addEventListener('scroll', handleScroll);
handleScroll();
return () => container.removeEventListener('scroll', handleScroll);
}, [containerRef, checkIfUserAtBottom]);
// Imperative scroll function
const scrollToBottom = useCallback(() => {
if (containerRef?.current) {
const container = containerRef.current;
container.scrollTo({
top: container.scrollHeight - container.clientHeight,
behavior: 'smooth',
});
}
}, [containerRef]);
useEffect(() => {
if (!messages) return;
if (!containerRef?.current) return;
requestAnimationFrame(() => {
setTimeout(() => {
if (isAtBottomRef.current) {
scrollToBottom();
}
}, 100);
});
}, [messages, containerRef, scrollToBottom]);
return { scrollRef: ref, isAtBottom, scrollToBottom };
};
export const useHandleMessageInputChange = () => {
const [value, setValue] = useState('');
const handleInputChange: ChangeEventHandler<HTMLTextAreaElement> = (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 messageContainerRef = useRef<HTMLDivElement>(null);
const { scrollRef, scrollToBottom } = useScrollToBottom(
derivedMessages,
messageContainerRef,
);
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 }),
},
];
});
},
[],
);
const addNewestOneQuestion = useCallback((message: Message) => {
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
},
];
});
}, []);
// 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'),
},
];
});
}, []);
// Add the streaming message to the last item in the message list
const addNewestOneAnswer = useCallback((answer: IAnswer) => {
setDerivedMessages((pre) => {
const idx = pre.findIndex((x) => x.id === answer.id);
if (idx !== -1) {
return pre.map((x) => {
if (x.id === answer.id) {
return { ...x, ...answer, content: answer.answer };
}
return x;
});
}
return [
...(pre ?? []),
{
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],
);
const removeAllMessages = useCallback(() => {
setDerivedMessages([]);
}, [setDerivedMessages]);
const removeAllMessagesExceptFirst = useCallback(() => {
setDerivedMessages((list) => {
if (list.length <= 1) {
return list;
}
return list.slice(0, 1);
});
}, [setDerivedMessages]);
return {
scrollRef,
messageContainerRef,
derivedMessages,
setDerivedMessages,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
addNewestOneQuestion,
addNewestOneAnswer,
removeMessagesAfterCurrentMessage,
removeAllMessages,
scrollToBottom,
removeAllMessagesExceptFirst,
};
};
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(true);
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]);
};

View File

@@ -0,0 +1,191 @@
import { AgentCategory, AgentQuery } from '@/constants/agent';
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
import { Routes } from '@/routes';
import { useCallback } from 'react';
import { useNavigate, useParams, useSearchParams } from 'umi';
export enum QueryStringMap {
KnowledgeId = 'knowledgeId',
id = 'id',
}
export const useNavigatePage = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { id } = useParams();
const navigateToDatasetList = useCallback(() => {
navigate(Routes.Datasets);
}, [navigate]);
const navigateToDataset = useCallback(
(id: string) => () => {
// navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
navigate(`${Routes.Dataset}/${id}`);
},
[navigate],
);
const navigateToDatasetOverview = useCallback(
(id: string) => () => {
navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
},
[navigate],
);
const navigateToDataFile = useCallback(
(id: string) => () => {
navigate(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`);
},
[navigate],
);
const navigateToHome = useCallback(() => {
navigate(Routes.Root);
}, [navigate]);
const navigateToProfile = useCallback(() => {
navigate(Routes.ProfileSetting);
}, [navigate]);
const navigateToOldProfile = useCallback(() => {
navigate(Routes.UserSetting);
}, [navigate]);
const navigateToChatList = useCallback(() => {
navigate(Routes.Chats);
}, [navigate]);
const navigateToChat = useCallback(
(id: string) => () => {
navigate(`${Routes.Chat}/${id}`);
},
[navigate],
);
const navigateToAgents = useCallback(() => {
navigate(Routes.Agents);
}, [navigate]);
const navigateToAgentList = useCallback(() => {
navigate(Routes.AgentList);
}, [navigate]);
const navigateToAgent = useCallback(
(id: string, category?: AgentCategory) => () => {
navigate(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
},
[navigate],
);
const navigateToDataflow = useCallback(
(id: string) => () => {
navigate(`${Routes.DataFlow}/${id}`);
},
[navigate],
);
const navigateToAgentLogs = useCallback(
(id: string) => () => {
navigate(`${Routes.AgentLogPage}/${id}`);
},
[navigate],
);
const navigateToAgentTemplates = useCallback(() => {
navigate(Routes.AgentTemplates);
}, [navigate]);
const navigateToSearchList = useCallback(() => {
navigate(Routes.Searches);
}, [navigate]);
const navigateToSearch = useCallback(
(id: string) => () => {
navigate(`${Routes.Search}/${id}`);
},
[navigate],
);
const navigateToChunkParsedResult = useCallback(
(id: string, knowledgeId?: string) => () => {
navigate(
`${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`,
// `${Routes.DataflowResult}?id=${knowledgeId}&doc_id=${id}&type=chunk`,
);
},
[navigate],
);
const getQueryString = useCallback(
(queryStringKey?: QueryStringMap) => {
const allQueryString = {
[QueryStringMap.KnowledgeId]: searchParams.get(
QueryStringMap.KnowledgeId,
),
[QueryStringMap.id]: searchParams.get(QueryStringMap.id),
};
if (queryStringKey) {
return allQueryString[queryStringKey];
}
return allQueryString;
},
[searchParams],
);
const navigateToChunk = useCallback(
(route: Routes) => {
navigate(
`${route}/${id}?${QueryStringMap.KnowledgeId}=${getQueryString(QueryStringMap.KnowledgeId)}`,
);
},
[getQueryString, id, navigate],
);
const navigateToFiles = useCallback(
(folderId?: string) => {
navigate(`${Routes.Files}?folderId=${folderId}`);
},
[navigate],
);
const navigateToDataflowResult = useCallback(
(props: NavigateToDataflowResultProps) => () => {
let params: string[] = [];
Object.keys(props).forEach((key) => {
if (props[key]) {
params.push(`${key}=${props[key]}`);
}
});
navigate(
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
`${Routes.DataflowResult}?${params.join('&')}`,
);
},
[navigate],
);
return {
navigateToDatasetList,
navigateToDataset,
navigateToDatasetOverview,
navigateToHome,
navigateToProfile,
navigateToChatList,
navigateToChat,
navigateToChunkParsedResult,
getQueryString,
navigateToChunk,
navigateToAgents,
navigateToAgent,
navigateToAgentLogs,
navigateToAgentTemplates,
navigateToSearchList,
navigateToSearch,
navigateToFiles,
navigateToAgentList,
navigateToOldProfile,
navigateToDataflowResult,
navigateToDataflow,
navigateToDataFile,
};
};

View File

@@ -0,0 +1,14 @@
import { useCallback, useState } from 'react';
export const useHandleSearchStrChange = () => {
const [searchString, setSearchString] = useState('');
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value;
setSearchString(value);
},
[],
);
return { handleInputChange, searchString };
};

View File

@@ -0,0 +1,24 @@
import { useCallback, useMemo, useState } from 'react';
export function useClientPagination(list: Array<any>) {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const onPaginationChange = useCallback((page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
}, []);
const pagedList = useMemo(() => {
return list?.slice((page - 1) * pageSize, page * pageSize);
}, [list, page, pageSize]);
return {
page,
pageSize,
setPage,
setPageSize,
onPaginationChange,
pagedList,
};
}

View File

@@ -0,0 +1,39 @@
import { RowSelectionState } from '@tanstack/react-table';
import { isEmpty } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
export function useRowSelection() {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const clearRowSelection = useCallback(() => {
setRowSelection({});
}, []);
const selectedCount = useMemo(() => {
return Object.keys(rowSelection).length;
}, [rowSelection]);
return {
rowSelection,
setRowSelection,
rowSelectionIsEmpty: isEmpty(rowSelection),
clearRowSelection,
selectedCount,
};
}
export type UseRowSelectionType = ReturnType<typeof useRowSelection>;
export function useSelectedIds<T extends Array<{ id: string }>>(
rowSelection: RowSelectionState,
list: T,
) {
const selectedIds = useMemo(() => {
const indexes = Object.keys(rowSelection);
return list
.filter((x, idx) => indexes.some((y) => Number(y) === idx))
.map((x) => x.id);
}, [list, rowSelection]);
return { selectedIds };
}

View File

@@ -0,0 +1,152 @@
import message from '@/components/ui/message';
import { Authorization } from '@/constants/authorization';
import userService, {
getLoginChannels,
loginWithChannel,
} from '@/services/user-service';
import authorizationUtil, { redirectToLogin } from '@/utils/authorization-util';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Form } from 'antd';
import { FormInstance } from 'antd/lib';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
export interface ILoginRequestBody {
email: string;
password: string;
}
export interface IRegisterRequestBody extends ILoginRequestBody {
nickname: string;
}
export interface ILoginChannel {
channel: string;
display_name: string;
icon: string;
}
export const useLoginChannels = () => {
const { data, isLoading } = useQuery({
queryKey: ['loginChannels'],
queryFn: async () => {
const { data: res = {} } = await getLoginChannels();
return res.data || [];
},
});
return { channels: data as ILoginChannel[], loading: isLoading };
};
export const useLoginWithChannel = () => {
const { isPending: loading, mutateAsync } = useMutation({
mutationKey: ['loginWithChannel'],
mutationFn: async (channel: string) => {
loginWithChannel(channel);
return Promise.resolve();
},
});
return { loading, login: mutateAsync };
};
export const useLogin = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['login'],
mutationFn: async (params: { email: string; password: string }) => {
const { data: res = {}, response } = await userService.login(params);
if (res.code === 0) {
const { data } = res;
const authorization = response.headers.get(Authorization);
const token = data.access_token;
const userInfo = {
avatar: data.avatar,
name: data.nickname,
email: data.email,
};
authorizationUtil.setItems({
Authorization: authorization,
userInfo: JSON.stringify(userInfo),
Token: token,
});
}
return res.code;
},
});
return { data, loading, login: mutateAsync };
};
export const useRegister = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['register'],
mutationFn: async (params: {
email: string;
password: string;
nickname: string;
}) => {
const { data = {} } = await userService.register(params);
if (data.code === 0) {
message.success(t('message.registered'));
} else if (
data.message &&
data.message.includes('registration is disabled')
) {
message.error(
t('message.registerDisabled') || 'User registration is disabled',
);
}
return data.code;
},
});
return { data, loading, register: mutateAsync };
};
export const useLogout = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['logout'],
mutationFn: async () => {
const { data = {} } = await userService.logout();
if (data.code === 0) {
message.success(t('message.logout'));
authorizationUtil.removeAll();
redirectToLogin();
}
return data.code;
},
});
return { data, loading, logout: mutateAsync };
};
export const useHandleSubmittable = (form: FormInstance) => {
const [submittable, setSubmittable] = useState<boolean>(false);
// Watch all values
const values = Form.useWatch([], form);
useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => setSubmittable(true))
.catch(() => setSubmittable(false));
}, [form, values]);
return { submittable };
};

View File

@@ -0,0 +1,17 @@
import { ILLMTools } from '@/interfaces/database/plugin';
import pluginService from '@/services/plugin-service';
import { useQuery } from '@tanstack/react-query';
export const useLlmToolsList = (): ILLMTools => {
const { data } = useQuery({
queryKey: ['llmTools'],
initialData: [],
queryFn: async () => {
const { data } = await pluginService.getLlmTools();
return data?.data ?? [];
},
});
return data;
};

View File

@@ -0,0 +1,91 @@
import {
KnowledgeRouteKey,
KnowledgeSearchParams,
} from '@/constants/knowledge';
import { useCallback } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'umi';
export enum SegmentIndex {
Second = '2',
Third = '3',
}
export const useSegmentedPathName = (index: SegmentIndex) => {
const { pathname } = useLocation();
const pathArray = pathname.split('/');
return pathArray[index] || '';
};
export const useSecondPathName = () => {
return useSegmentedPathName(SegmentIndex.Second);
};
export const useThirdPathName = () => {
return useSegmentedPathName(SegmentIndex.Third);
};
export const useGetKnowledgeSearchParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
type: currentQueryParameters.get(KnowledgeSearchParams.Type) || '',
documentId:
currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '',
knowledgeId:
currentQueryParameters.get(KnowledgeSearchParams.KnowledgeId) || '',
};
};
export const useNavigateWithFromState = () => {
const navigate = useNavigate();
return useCallback(
(path: string) => {
navigate(path, { state: { from: path } });
},
[navigate],
);
};
export const useNavigateToDataset = () => {
const navigate = useNavigate();
const { knowledgeId } = useGetKnowledgeSearchParams();
return useCallback(() => {
navigate(`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeId}`);
}, [knowledgeId, navigate]);
};
export const useGetPaginationParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
page: currentQueryParameters.get('page') || 1,
size: currentQueryParameters.get('size') || 10,
};
};
export const useSetPaginationParams = () => {
const [queryParameters, setSearchParams] = useSearchParams();
// const newQueryParameters: URLSearchParams = useMemo(
// () => new URLSearchParams(queryParameters.toString()),
// [queryParameters],
// );
const setPaginationParams = useCallback(
(page: number = 1, pageSize?: number) => {
queryParameters.set('page', page.toString());
if (pageSize) {
queryParameters.set('size', pageSize.toString());
}
setSearchParams(queryParameters);
},
[setSearchParams, queryParameters],
);
return {
setPaginationParams,
page: Number(queryParameters.get('page')) || 1,
size: Number(queryParameters.get('size')) || 50,
};
};

View File

@@ -0,0 +1,18 @@
import userService from '@/services/user-service';
import { useQuery } from '@tanstack/react-query';
/**
* Hook to fetch system configuration including register enable status
* @returns System configuration with loading status
*/
export const useSystemConfig = () => {
const { data, isLoading } = useQuery({
queryKey: ['systemConfig'],
queryFn: async () => {
const { data = {} } = await userService.getSystemConfig();
return data.data || { registerEnabled: 1 }; // Default to enabling registration
},
});
return { config: data, loading: isLoading };
};

View File

@@ -0,0 +1,736 @@
import { FileUploadProps } from '@/components/file-upload';
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
import message from '@/components/ui/message';
import { AgentGlobals } from '@/constants/agent';
import {
IAgentLogsRequest,
IAgentLogsResponse,
IFlow,
IFlowTemplate,
IPipeLineListRequest,
ITraceData,
} from '@/interfaces/database/agent';
import { IDebugSingleRequestBody } from '@/interfaces/request/agent';
import i18n from '@/locales/config';
import { BeginId } from '@/pages/agent/constant';
import { IInputs } from '@/pages/agent/interface';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import agentService, {
fetchAgentLogsByCanvasId,
fetchPipeLineList,
fetchTrace,
} from '@/services/agent-service';
import api from '@/utils/api';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { get, set } from 'lodash';
import { useCallback, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
export const enum AgentApiAction {
FetchAgentListByPage = 'fetchAgentListByPage',
FetchAgentList = 'fetchAgentList',
UpdateAgentSetting = 'updateAgentSetting',
DeleteAgent = 'deleteAgent',
FetchAgentDetail = 'fetchAgentDetail',
ResetAgent = 'resetAgent',
SetAgent = 'setAgent',
FetchAgentTemplates = 'fetchAgentTemplates',
UploadCanvasFile = 'uploadCanvasFile',
UploadCanvasFileWithProgress = 'uploadCanvasFileWithProgress',
Trace = 'trace',
TestDbConnect = 'testDbConnect',
DebugSingle = 'debugSingle',
FetchInputForm = 'fetchInputForm',
FetchVersionList = 'fetchVersionList',
FetchVersion = 'fetchVersion',
FetchAgentAvatar = 'fetchAgentAvatar',
FetchExternalAgentInputs = 'fetchExternalAgentInputs',
SetAgentSetting = 'setAgentSetting',
FetchPrompt = 'fetchPrompt',
CancelDataflow = 'cancelDataflow',
}
export const EmptyDsl = {
graph: {
nodes: [
{
id: BeginId,
type: 'beginNode',
position: {
x: 50,
y: 200,
},
data: {
label: 'Begin',
name: 'begin',
},
sourcePosition: 'left',
targetPosition: 'right',
},
],
edges: [],
},
components: {
begin: {
obj: {
component_name: 'Begin',
params: {},
},
downstream: [], // other edge target is downstream, edge source is current node id
upstream: [], // edge source is upstream, edge target is current node id
},
},
retrieval: [], // reference
history: [],
path: [],
globals: {
[AgentGlobals.SysQuery]: '',
[AgentGlobals.SysUserId]: '',
[AgentGlobals.SysConversationTurns]: 0,
[AgentGlobals.SysFiles]: [],
},
};
export const useFetchAgentTemplates = () => {
const { data } = useQuery<IFlowTemplate[]>({
queryKey: [AgentApiAction.FetchAgentTemplates],
initialData: [],
queryFn: async () => {
const { data } = await agentService.listTemplates();
return data.data;
},
});
return data;
};
export const useFetchAgentListByPage = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { filterValue, handleFilterSubmit } = useHandleFilterSubmit();
const canvasCategory = Array.isArray(filterValue.canvasCategory)
? filterValue.canvasCategory
: [];
const owner = filterValue.owner;
const requestParams: Record<string, any> = {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
canvas_category:
canvasCategory.length === 1 ? canvasCategory[0] : undefined,
};
if (Array.isArray(owner) && owner.length > 0) {
requestParams.owner_ids = owner.join(',');
}
const { data, isFetching: loading } = useQuery<{
canvas: IFlow[];
total: number;
}>({
queryKey: [
AgentApiAction.FetchAgentListByPage,
{
debouncedSearchString,
...pagination,
filterValue,
},
],
placeholderData: (previousData) => {
if (previousData === undefined) {
return { canvas: [], total: 0 };
}
return previousData;
},
gcTime: 0,
queryFn: async () => {
const { data } = await agentService.listCanvas(
{
params: requestParams,
},
true,
);
return data?.data;
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
// setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange],
);
return {
data: data?.canvas ?? [],
loading,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
setPagination,
filterValue,
handleFilterSubmit,
};
};
export const useUpdateAgentSetting = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.UpdateAgentSetting],
mutationFn: async (params: any) => {
const ret = await agentService.settingCanvas(params);
if (ret?.data?.code === 0) {
message.success('success');
queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentListByPage],
});
} else {
message.error(ret?.data?.data);
}
return ret?.data?.code;
},
});
return { data, loading, updateAgentSetting: mutateAsync };
};
export const useDeleteAgent = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.DeleteAgent],
mutationFn: async (canvasIds: string[]) => {
const { data } = await agentService.removeCanvas({ canvasIds });
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentListByPage],
});
}
return data?.data ?? [];
},
});
return { data, loading, deleteAgent: mutateAsync };
};
export const useFetchAgent = (): {
data: IFlow;
loading: boolean;
refetch: () => void;
} => {
const { id } = useParams();
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery({
queryKey: [AgentApiAction.FetchAgentDetail],
initialData: {} as IFlow,
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
queryFn: async () => {
const { data } = await agentService.fetchCanvas(sharedId || id);
const messageList = buildMessageListWithUuid(
get(data, 'data.dsl.messages', []),
);
set(data, 'data.dsl.messages', messageList);
return data?.data ?? {};
},
});
return { data, loading, refetch };
};
export const useResetAgent = () => {
const { id } = useParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.ResetAgent],
mutationFn: async () => {
const { data } = await agentService.resetCanvas({ id });
return data;
},
});
return { data, loading, resetAgent: mutateAsync };
};
export const useSetAgent = (showMessage: boolean = true) => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.SetAgent],
mutationFn: async (params: {
id?: string;
title?: string;
dsl?: Record<string, any>;
avatar?: string;
canvas_category?: string;
}) => {
const { data = {} } = await agentService.setCanvas(params);
if (data.code === 0) {
if (showMessage) {
message.success(
i18n.t(`message.${params?.id ? 'modified' : 'created'}`),
);
}
queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentListByPage],
});
}
return data;
},
});
return { data, loading, setAgent: mutateAsync };
};
// 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,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.UploadCanvasFile],
mutationFn: async (body: any) => {
let nextBody = body;
try {
if (Array.isArray(body)) {
nextBody = new FormData();
body.forEach((file: File) => {
nextBody.append('file', file as any);
});
}
const { data } = await agentService.uploadCanvasFile(
{ url: api.uploadAgentFile(canvasId as string), data: nextBody },
true,
);
if (data?.code === 0) {
message.success(i18n.t('message.uploaded'));
}
return data;
} catch (error) {
message.error('error');
}
},
});
return { data, loading, uploadCanvasFile: mutateAsync };
};
export const useUploadCanvasFileWithProgress = (
identifier?: Nullable<string>,
) => {
const { id } = useParams();
type UploadParameters = Parameters<NonNullable<FileUploadProps['onUpload']>>;
type X = { files: UploadParameters[0]; options: UploadParameters[1] };
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.UploadCanvasFileWithProgress],
mutationFn: async ({
files,
options: { onError, onSuccess, onProgress },
}: X) => {
const formData = new FormData();
try {
if (Array.isArray(files)) {
files.forEach((file: File) => {
formData.append('file', file);
});
}
const { data } = await agentService.uploadCanvasFile(
{
url: api.uploadAgentFile(identifier || id),
data: formData,
onUploadProgress: ({ progress }) => {
files.forEach((file) => {
onProgress(file, (progress || 0) * 100);
});
},
},
true,
);
if (data?.code === 0) {
files.forEach((file) => {
onSuccess(file);
});
message.success(i18n.t('message.uploaded'));
}
return data;
} catch (error) {
files.forEach((file) => {
onError(file, error as Error);
});
message.error((error as Error)?.message || 'Upload failed');
}
},
});
return { data, loading, uploadCanvasFile: mutateAsync };
};
export const useFetchMessageTrace = (canvasId?: string) => {
const { id } = useParams();
const queryId = id || canvasId;
const [messageId, setMessageId] = useState('');
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITraceData[]>({
queryKey: [AgentApiAction.Trace, queryId, messageId],
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
enabled: !!queryId && !!messageId,
refetchInterval: !isStopFetchTrace ? 3000 : false,
queryFn: async () => {
const { data } = await fetchTrace({
canvas_id: queryId as string,
message_id: messageId,
});
return Array.isArray(data?.data) ? data?.data : [];
},
});
return {
data,
loading,
refetch,
setMessageId,
messageId,
isStopFetchTrace,
setISStopFetchTrace,
};
};
export const useTestDbConnect = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.TestDbConnect],
mutationFn: async (params: any) => {
const ret = await agentService.testDbConnect(params);
if (ret?.data?.code === 0) {
message.success(ret?.data?.data);
} else {
message.error(ret?.data?.data);
}
return ret;
},
});
return { data, loading, testDbConnect: mutateAsync };
};
export const useDebugSingle = () => {
const { id } = useParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.FetchInputForm],
mutationFn: async (params: IDebugSingleRequestBody) => {
const ret = await agentService.debugSingle({ id, ...params });
if (ret?.data?.code !== 0) {
message.error(ret?.data?.message);
}
return ret?.data?.data;
},
});
return { data, loading, debugSingle: mutateAsync };
};
export const useFetchInputForm = (componentId?: string) => {
const { id } = useParams();
const { data } = useQuery<Record<string, any>>({
queryKey: [AgentApiAction.FetchInputForm],
initialData: {},
enabled: !!id && !!componentId,
queryFn: async () => {
const { data } = await agentService.inputForm(
{
params: {
id,
component_id: componentId,
},
},
true,
);
return data.data;
},
});
return data;
};
export const useFetchVersionList = () => {
const { id } = useParams();
const { data, isFetching: loading } = useQuery<
Array<{ created_at: string; title: string; id: string }>
>({
queryKey: [AgentApiAction.FetchVersionList],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await agentService.fetchVersionList(id);
return data?.data ?? [];
},
});
return { data, loading };
};
export const useFetchVersion = (
version_id?: string,
): {
data?: IFlow;
loading: boolean;
} => {
const { data, isFetching: loading } = useQuery({
queryKey: [AgentApiAction.FetchVersion, version_id],
initialData: undefined,
gcTime: 0,
enabled: !!version_id, // Only call API when both values are provided
queryFn: async () => {
if (!version_id) return undefined;
const { data } = await agentService.fetchVersion(version_id);
return data?.data ?? undefined;
},
});
return { data, loading };
};
export const useFetchAgentAvatar = (): {
data: IFlow;
loading: boolean;
refetch: () => void;
} => {
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery({
queryKey: [AgentApiAction.FetchAgentAvatar],
initialData: {} as IFlow,
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
queryFn: async () => {
if (!sharedId) return {};
const { data } = await agentService.fetchAgentAvatar(sharedId);
return data?.data ?? {};
},
});
return { data, loading, refetch };
};
export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => {
const { id } = useParams();
const { data, isFetching: loading } = useQuery<IAgentLogsResponse>({
queryKey: ['fetchAgentLog', id, searchParams],
initialData: {} as IAgentLogsResponse,
gcTime: 0,
queryFn: async () => {
const { data } = await fetchAgentLogsByCanvasId(id as string, {
...searchParams,
});
return data?.data ?? [];
},
});
return { data, loading };
};
export const useFetchExternalAgentInputs = () => {
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IInputs>({
queryKey: [AgentApiAction.FetchExternalAgentInputs],
initialData: {} as IInputs,
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
enabled: !!sharedId,
queryFn: async () => {
const { data } = await agentService.fetchExternalAgentInputs(sharedId!);
return data?.data ?? {};
},
});
return { data, loading, refetch };
};
export const useSetAgentSetting = () => {
const { id } = useParams();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.SetAgentSetting],
mutationFn: async (params: any) => {
const ret = await agentService.settingCanvas({ id, ...params });
if (ret?.data?.code === 0) {
message.success('success');
queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentDetail],
});
} else {
message.error(ret?.data?.data);
}
return ret?.data?.code;
},
});
return { data, loading, setAgentSetting: mutateAsync };
};
export const useFetchPrompt = () => {
const {
data,
isFetching: loading,
refetch,
} = useQuery<Record<string, string>>({
queryKey: [AgentApiAction.FetchPrompt],
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
queryFn: async () => {
const { data } = await agentService.fetchPrompt();
return data?.data ?? {};
},
});
return { data, loading, refetch };
};
export const useFetchAgentList = ({
canvas_category,
}: IPipeLineListRequest) => {
const { data, isFetching: loading } = useQuery<{
canvas: IFlow[];
total: number;
}>({
queryKey: [AgentApiAction.FetchAgentList],
initialData: { canvas: [], total: 0 },
gcTime: 0,
queryFn: async () => {
const { data } = await fetchPipeLineList({ canvas_category });
return data?.data ?? [];
},
});
return { data, loading };
};
export const useCancelDataflow = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [AgentApiAction.CancelDataflow],
mutationFn: async (taskId: string) => {
const ret = await agentService.cancelDataflow(taskId);
if (ret?.data?.code === 0) {
message.success('success');
} else {
message.error(ret?.data?.data);
}
return ret?.data?.code;
},
});
return { data, loading, cancelDataflow: mutateAsync };
};
// export const useFetchKnowledgeList = () => {
// const { data, isFetching: loading } = useQuery<IFlow[]>({
// queryKey: [AgentApiAction.FetchAgentList],
// initialData: [],
// gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
// queryFn: async () => {
// const { data } = await agentService.listCanvas();
// return data?.data ?? [];
// },
// });
// return { list: data, loading };
// };

View File

@@ -0,0 +1,27 @@
import * as React from 'react';
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx
*/
/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
function useCallbackRef<T extends (...args: never[]) => unknown>(
callback: T | undefined,
): T {
const callbackRef = React.useRef(callback);
React.useEffect(() => {
callbackRef.current = callback;
});
// https://github.com/facebook/react/issues/19240
return React.useMemo(
() => ((...args) => callbackRef.current?.(...args)) as T,
[],
);
}
export { useCallbackRef };

View File

@@ -0,0 +1,532 @@
import { FileUploadProps } from '@/components/file-upload';
import message from '@/components/ui/message';
import { ChatSearchParams } from '@/constants/chat';
import {
IConversation,
IDialog,
IExternalChatInfo,
} from '@/interfaces/database/chat';
import { IAskRequestBody } from '@/interfaces/request/chat';
import { IClientConversation } from '@/pages/next-chats/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
import { isConversationIdExist } from '@/pages/next-chats/utils';
import chatService from '@/services/next-chat-service';
import { buildMessageListWithUuid, getConversationId } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { has } from 'lodash';
import { useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { useHandleSearchStrChange } from './logic-hooks/use-change-search';
export const enum ChatApiAction {
FetchDialogList = 'fetchDialogList',
RemoveDialog = 'removeDialog',
SetDialog = 'setDialog',
FetchDialog = 'fetchDialog',
FetchConversationList = 'fetchConversationList',
FetchConversation = 'fetchConversation',
UpdateConversation = 'updateConversation',
RemoveConversation = 'removeConversation',
DeleteMessage = 'deleteMessage',
FetchMindMap = 'fetchMindMap',
FetchRelatedQuestions = 'fetchRelatedQuestions',
UploadAndParse = 'upload_and_parse',
FetchExternalChatInfo = 'fetchExternalChatInfo',
}
export const useGetChatSearchParams = () => {
const [currentQueryParameters] = useSearchParams();
return {
dialogId: currentQueryParameters.get(ChatSearchParams.DialogId) || '',
conversationId:
currentQueryParameters.get(ChatSearchParams.ConversationId) || '',
isNew: currentQueryParameters.get(ChatSearchParams.isNew) || '',
};
};
export const useClickDialogCard = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(() => {
return new URLSearchParams();
}, []);
const handleClickDialog = useCallback(
(dialogId: string) => {
newQueryParameters.set(ChatSearchParams.DialogId, dialogId);
// newQueryParameters.set(
// ChatSearchParams.ConversationId,
// EmptyConversationId,
// );
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
return { handleClickDialog };
};
export const useFetchDialogList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const {
data,
isFetching: loading,
refetch,
} = useQuery<{ dialogs: IDialog[]; total: number }>({
queryKey: [
ChatApiAction.FetchDialogList,
{
debouncedSearchString,
...pagination,
},
],
initialData: { dialogs: [], total: 0 },
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.listDialog(
{
params: {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
data: {},
},
true,
);
return data?.data ?? { dialogs: [], total: 0 };
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
handleInputChange(e);
},
[handleInputChange],
);
return {
data,
loading,
refetch,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
setPagination,
};
};
export const useRemoveDialog = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.RemoveDialog],
mutationFn: async (dialogIds: string[]) => {
const { data } = await chatService.removeDialog({ dialogIds });
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchDialogList'] });
message.success(t('message.deleted'));
}
return data.code;
},
});
return { data, loading, removeDialog: mutateAsync };
};
export const useSetDialog = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.SetDialog],
mutationFn: async (params: Partial<IDialog>) => {
const { data } = await chatService.setDialog(params);
if (data.code === 0) {
queryClient.invalidateQueries({
exact: false,
queryKey: [ChatApiAction.FetchDialogList],
});
queryClient.invalidateQueries({
queryKey: [ChatApiAction.FetchDialog],
});
message.success(
t(`message.${params.dialog_id ? 'modified' : 'created'}`),
);
}
return data?.code;
},
});
return { data, loading, setDialog: mutateAsync };
};
export const useFetchDialog = () => {
const { id } = useParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IDialog>({
queryKey: [ChatApiAction.FetchDialog, id],
gcTime: 0,
initialData: {} as IDialog,
enabled: !!id,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.getDialog(
{ params: { dialogId: id } },
true,
);
return data?.data ?? ({} as IDialog);
},
});
return { data, loading, refetch };
};
//#region Conversation
export const useClickConversationCard = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const handleClickConversation = useCallback(
(conversationId: string, isNew: string) => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
newQueryParameters.set(ChatSearchParams.isNew, isNew);
setSearchParams(newQueryParameters);
},
[setSearchParams, newQueryParameters],
);
return { handleClickConversation };
};
export const useFetchConversationList = () => {
const { id } = useParams();
const { handleClickConversation } = useClickConversationCard();
const { searchString, handleInputChange } = useHandleSearchStrChange();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IConversation[]>({
queryKey: [ChatApiAction.FetchConversationList, id],
initialData: [],
gcTime: 0,
refetchOnWindowFocus: false,
enabled: !!id,
select(data) {
return searchString
? data.filter((x) => x.name.includes(searchString))
: data;
},
queryFn: async () => {
const { data } = await chatService.listConversation(
{ params: { dialog_id: id } },
true,
);
if (data.code === 0) {
if (data.data.length > 0) {
handleClickConversation(data.data[0].id, '');
} else {
handleClickConversation('', '');
}
}
return data?.data;
},
});
return { data, loading, refetch, searchString, handleInputChange };
};
export const useFetchConversation = () => {
const { isNew, conversationId } = useGetChatSearchParams();
const { sharedId } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IClientConversation>({
queryKey: [ChatApiAction.FetchConversation, conversationId],
initialData: {} as IClientConversation,
// enabled: isConversationIdExist(conversationId),
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
if (
isNew !== 'true' &&
isConversationIdExist(sharedId || conversationId)
) {
const { data } = await chatService.getConversation(
{
params: {
conversationId: conversationId || sharedId,
},
},
true,
);
const conversation = data?.data ?? {};
const messageList = buildMessageListWithUuid(conversation?.message);
return { ...conversation, message: messageList };
}
return { message: [] };
},
});
return { data, loading, refetch };
};
export const useUpdateConversation = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.UpdateConversation],
mutationFn: async (params: Record<string, any>) => {
const { data } = await chatService.setConversation({
...params,
conversation_id: params.conversation_id
? params.conversation_id
: getConversationId(),
});
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [ChatApiAction.FetchConversationList],
});
message.success(t(`message.modified`));
}
return data;
},
});
return { data, loading, updateConversation: mutateAsync };
};
export const useRemoveConversation = () => {
const queryClient = useQueryClient();
const { dialogId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.RemoveConversation],
mutationFn: async (conversationIds: string[]) => {
const { data } = await chatService.removeConversation({
conversationIds,
dialogId,
});
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [ChatApiAction.FetchConversationList],
});
}
return data.code;
},
});
return { data, loading, removeConversation: mutateAsync };
};
export const useDeleteMessage = () => {
const { conversationId } = useGetChatSearchParams();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.DeleteMessage],
mutationFn: async (messageId: string) => {
const { data } = await chatService.deleteMessage({
messageId,
conversationId,
});
if (data.code === 0) {
message.success(t(`message.deleted`));
}
return data.code;
},
});
return { data, loading, deleteMessage: mutateAsync };
};
type UploadParameters = Parameters<NonNullable<FileUploadProps['onUpload']>>;
type X = {
file: UploadParameters[0][0];
options: UploadParameters[1];
conversationId?: string;
};
export function useUploadAndParseFile() {
const { conversationId: id } = useGetChatSearchParams();
const { t } = useTranslation();
const controller = useRef(new AbortController());
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.UploadAndParse],
mutationFn: async ({
file,
options: { onProgress, onSuccess, onError },
conversationId,
}: X) => {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('conversation_id', conversationId || id);
const { data } = await chatService.uploadAndParse(
{
signal: controller.current.signal,
data: formData,
onUploadProgress: ({ progress }) => {
onProgress(file, (progress || 0) * 100 - 1);
},
},
true,
);
onProgress(file, 100);
if (data.code === 0) {
onSuccess(file);
message.success(t(`message.uploaded`));
} else {
onError(file, new Error(data.message));
}
return data;
} catch (error) {
onError(file, error as Error);
}
},
});
const cancel = useCallback(() => {
controller.current.abort();
controller.current = new AbortController();
}, [controller]);
return { data, loading, uploadAndParseFile: mutateAsync, cancel };
}
export const useFetchExternalChatInfo = () => {
const { sharedId: id } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IExternalChatInfo>({
queryKey: [ChatApiAction.FetchExternalChatInfo, id],
gcTime: 0,
initialData: {} as IExternalChatInfo,
enabled: !!id,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.fetchExternalChatInfo(id!);
return data?.data;
},
});
return { data, loading, refetch };
};
//#endregion
//#region search page
export const useFetchMindMap = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.FetchMindMap],
gcTime: 0,
mutationFn: async (params: IAskRequestBody) => {
try {
const ret = await chatService.getMindMap(params);
return ret?.data?.data ?? {};
} catch (error: any) {
if (has(error, 'message')) {
message.error(error.message);
}
return [];
}
},
});
return { data, loading, fetchMindMap: mutateAsync };
};
export const useFetchRelatedQuestions = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.FetchRelatedQuestions],
gcTime: 0,
mutationFn: async (question: string): Promise<string[]> => {
const { data } = await chatService.getRelatedQuestions({ question });
return data?.data ?? [];
},
});
return { data, loading, fetchRelatedQuestions: mutateAsync };
};
//#endregion

View File

@@ -0,0 +1,120 @@
import message from '@/components/ui/message';
import { ResponseGetType } from '@/interfaces/database/base';
import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IChunkListResult } from './chunk-hooks';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { useGetKnowledgeSearchParams } from './route-hook';
export const useFetchNextChunkList = (
enabled = true,
): ResponseGetType<{
data: IChunk[];
total: number;
documentInfo: IKnowledgeFile;
}> &
IChunkListResult => {
const { pagination, setPagination } = useGetPaginationWithRouter();
const { documentId } = useGetKnowledgeSearchParams();
const { searchString, handleInputChange } = useHandleSearchChange();
const [available, setAvailable] = useState<number | undefined>();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { data, isFetching: loading } = useQuery({
queryKey: [
'fetchChunkList',
documentId,
pagination.current,
pagination.pageSize,
debouncedSearchString,
available,
],
placeholderData: (previousData: any) =>
previousData ?? { data: [], total: 0, documentInfo: {} }, // https://github.com/TanStack/query/issues/8183
gcTime: 0,
enabled,
queryFn: async () => {
const { data } = await kbService.chunk_list({
doc_id: documentId,
page: pagination.current,
size: pagination.pageSize,
available_int: available,
keywords: searchString,
});
if (data.code === 0) {
const res = data.data;
return {
data: res.chunks,
total: res.total,
documentInfo: res.doc,
};
}
return (
data?.data ?? {
data: [],
total: 0,
documentInfo: {},
}
);
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);
const handleSetAvailable = useCallback(
(a: number | undefined) => {
setPagination({ page: 1 });
setAvailable(a);
},
[setAvailable, setPagination],
);
return {
data,
loading,
pagination,
setPagination,
searchString,
handleInputChange: onInputChange,
available,
handleSetAvailable,
};
};
export const useSwitchChunk = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['switchChunk'],
mutationFn: async (params: {
chunk_ids?: string[];
available_int?: number;
doc_id: string;
}) => {
const { data } = await kbService.switch_chunk(params);
if (data.code === 0) {
message.success(t('message.modified'));
}
return data?.code;
},
});
return { data, loading, switchChunk: mutateAsync };
};

View File

@@ -0,0 +1,67 @@
import * as React from 'react';
import { useCallbackRef } from '@/hooks/use-callback-ref';
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx
*/
type UseControllableStateParams<T> = {
prop?: T | undefined;
defaultProp?: T | undefined;
onChange?: (state: T) => void;
};
type SetStateFn<T> = (prevState?: T) => T;
function useUncontrolledState<T>({
defaultProp,
onChange,
}: Omit<UseControllableStateParams<T>, 'prop'>) {
const uncontrolledState = React.useState<T | undefined>(defaultProp);
const [value] = uncontrolledState;
const prevValueRef = React.useRef(value);
const handleChange = useCallbackRef(onChange);
React.useEffect(() => {
if (prevValueRef.current !== value) {
handleChange(value as T);
prevValueRef.current = value;
}
}, [value, prevValueRef, handleChange]);
return uncontrolledState;
}
function useControllableState<T>({
prop,
defaultProp,
onChange = () => {},
}: UseControllableStateParams<T>) {
const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({
defaultProp,
onChange,
});
const isControlled = prop !== undefined;
const value = isControlled ? prop : uncontrolledProp;
const handleChange = useCallbackRef(onChange);
const setValue: React.Dispatch<React.SetStateAction<T | undefined>> =
React.useCallback(
(nextValue) => {
if (isControlled) {
const setter = nextValue as SetStateFn<T>;
const value =
typeof nextValue === 'function' ? setter(prop) : nextValue;
if (value !== prop) handleChange(value as T);
} else {
setUncontrolledProp(nextValue);
}
},
[isControlled, prop, setUncontrolledProp, handleChange],
);
return [value, setValue] as const;
}
export { useControllableState };

View File

@@ -0,0 +1,91 @@
import message from '@/components/ui/message';
import { IFlow } from '@/interfaces/database/agent';
import dataflowService from '@/services/dataflow-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
export const enum DataflowApiAction {
ListDataflow = 'listDataflow',
RemoveDataflow = 'removeDataflow',
FetchDataflow = 'fetchDataflow',
RunDataflow = 'runDataflow',
SetDataflow = 'setDataflow',
}
export const useRemoveDataflow = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DataflowApiAction.RemoveDataflow],
mutationFn: async (ids: string[]) => {
const { data } = await dataflowService.removeDataflow({
canvas_ids: ids,
});
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [DataflowApiAction.ListDataflow],
});
message.success(t('message.deleted'));
}
return data.code;
},
});
return { data, loading, removeDataflow: mutateAsync };
};
export const useSetDataflow = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DataflowApiAction.SetDataflow],
mutationFn: async (params: Partial<IFlow>) => {
const { data } = await dataflowService.setDataflow(params);
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [DataflowApiAction.FetchDataflow],
});
message.success(t(`message.${params.id ? 'modified' : 'created'}`));
}
return data?.code;
},
});
return { data, loading, setDataflow: mutateAsync };
};
export const useFetchDataflow = () => {
const { id } = useParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IFlow>({
queryKey: [DataflowApiAction.FetchDataflow, id],
gcTime: 0,
initialData: {} as IFlow,
enabled: !!id,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await dataflowService.fetchDataflow(id);
return data?.data ?? ({} as IFlow);
},
});
return { data, loading, refetch };
};

View File

@@ -0,0 +1,432 @@
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
import message from '@/components/ui/message';
import { ResponseType } from '@/interfaces/database/base';
import {
IDocumentInfo,
IDocumentInfoFilter,
} from '@/interfaces/database/document';
import {
IChangeParserConfigRequestBody,
IDocumentMetaRequestBody,
} from '@/interfaces/request/document';
import i18n from '@/locales/config';
import kbService, { listDocument } from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { get } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import {
useGetKnowledgeSearchParams,
useSetPaginationParams,
} from './route-hook';
export const enum DocumentApiAction {
UploadDocument = 'uploadDocument',
FetchDocumentList = 'fetchDocumentList',
UpdateDocumentStatus = 'updateDocumentStatus',
RunDocumentByIds = 'runDocumentByIds',
RemoveDocument = 'removeDocument',
SaveDocumentName = 'saveDocumentName',
SetDocumentParser = 'setDocumentParser',
SetDocumentMeta = 'setDocumentMeta',
FetchDocumentFilter = 'fetchDocumentFilter',
CreateDocument = 'createDocument',
}
export const useUploadNextDocument = () => {
const queryClient = useQueryClient();
const { id } = useParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation<ResponseType<IDocumentInfo[]>, Error, File[]>({
mutationKey: [DocumentApiAction.UploadDocument],
mutationFn: async (fileList) => {
const formData = new FormData();
formData.append('kb_id', id!);
fileList.forEach((file: any) => {
formData.append('file', file);
});
try {
const ret = await kbService.document_upload(formData);
const code = get(ret, 'data.code');
if (code === 0 || code === 500) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
}
return ret?.data;
} catch (error) {
console.warn(error);
return {
code: 500,
message: error + '',
};
}
},
});
return { uploadDocument: mutateAsync, loading, data };
};
export const useFetchDocumentList = () => {
const { knowledgeId } = useGetKnowledgeSearchParams();
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const { id } = useParams();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { filterValue, handleFilterSubmit } = useHandleFilterSubmit();
const [docs, setDocs] = useState<IDocumentInfo[]>([]);
const isLoop = useMemo(() => {
return docs.some((doc) => doc.run === '1');
}, [docs]);
const { data, isFetching: loading } = useQuery<{
docs: IDocumentInfo[];
total: number;
}>({
queryKey: [
DocumentApiAction.FetchDocumentList,
debouncedSearchString,
pagination,
filterValue,
],
initialData: { docs: [], total: 0 },
refetchInterval: isLoop ? 5000 : false,
enabled: !!knowledgeId || !!id,
queryFn: async () => {
const ret = await listDocument(
{
kb_id: knowledgeId || id,
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
{
suffix: filterValue.type,
run_status: filterValue.run,
},
);
if (ret.data.code === 0) {
return ret.data.data;
}
return {
docs: [],
total: 0,
};
},
});
useMemo(() => {
setDocs(data.docs);
}, [data.docs]);
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);
return {
loading,
searchString,
documents: data.docs,
pagination: { ...pagination, total: data?.total },
handleInputChange: onInputChange,
setPagination,
filterValue,
handleFilterSubmit,
};
};
// get document filter
export const useGetDocumentFilter = (): {
filter: IDocumentInfoFilter;
onOpenChange: (open: boolean) => void;
} => {
const { knowledgeId } = useGetKnowledgeSearchParams();
const { searchString } = useHandleSearchChange();
const { id } = useParams();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const [open, setOpen] = useState<number>(0);
const { data } = useQuery({
queryKey: [
DocumentApiAction.FetchDocumentFilter,
debouncedSearchString,
knowledgeId,
open,
],
queryFn: async () => {
const { data } = await kbService.documentFilter({
kb_id: knowledgeId || id,
keywords: debouncedSearchString,
});
if (data.code === 0) {
return data.data;
}
},
});
const handleOnpenChange = (e: boolean) => {
if (e) {
const currentOpen = open + 1;
setOpen(currentOpen);
}
};
return {
filter: data?.filter || {
run_status: {},
suffix: {},
},
onOpenChange: handleOnpenChange,
};
};
// update document status
export const useSetDocumentStatus = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.UpdateDocumentStatus],
mutationFn: async ({
status,
documentId,
}: {
status: boolean;
documentId: string | string[];
}) => {
const ids = Array.isArray(documentId) ? documentId : [documentId];
const { data } = await kbService.document_change_status({
doc_ids: ids,
status: Number(status),
});
if (data.code === 0) {
message.success(i18n.t('message.modified'));
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
}
return data;
},
});
return { setDocumentStatus: mutateAsync, data, loading };
};
// This hook is used to run a document by its IDs
export const useRunDocument = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.RunDocumentByIds],
mutationFn: async ({
documentIds,
run,
shouldDelete,
}: {
documentIds: string[];
run: number;
shouldDelete: boolean;
}) => {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
const ret = await kbService.document_run({
doc_ids: documentIds,
run,
delete: shouldDelete,
});
const code = get(ret, 'data.code');
if (code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(i18n.t('message.operated'));
}
return code;
},
});
return { runDocumentByIds: mutateAsync, loading, data };
};
export const useRemoveDocument = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.RemoveDocument],
mutationFn: async (documentIds: string | string[]) => {
const { data } = await kbService.document_rm({ doc_id: documentIds });
if (data.code === 0) {
message.success(i18n.t('message.deleted'));
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
}
return data.code;
},
});
return { data, loading, removeDocument: mutateAsync };
};
export const useSaveDocumentName = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SaveDocumentName],
mutationFn: async ({
name,
documentId,
}: {
name: string;
documentId: string;
}) => {
const { data } = await kbService.document_rename({
doc_id: documentId,
name: name,
});
if (data.code === 0) {
message.success(i18n.t('message.renamed'));
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
}
return data.code;
},
});
return { loading, saveName: mutateAsync, data };
};
export const useSetDocumentParser = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SetDocumentParser],
mutationFn: async ({
parserId,
pipelineId,
documentId,
parserConfig,
}: {
parserId: string;
pipelineId: string;
documentId: string;
parserConfig: IChangeParserConfigRequestBody;
}) => {
const { data } = await kbService.document_change_parser({
parser_id: parserId,
pipeline_id: pipelineId,
doc_id: documentId,
parser_config: parserConfig,
});
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(i18n.t('message.modified'));
}
return data.code;
},
});
return { setDocumentParser: mutateAsync, data, loading };
};
export const useSetDocumentMeta = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SetDocumentMeta],
mutationFn: async (params: IDocumentMetaRequestBody) => {
try {
const { data } = await kbService.setMeta({
meta: params.meta,
doc_id: params.documentId,
});
if (data?.code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(i18n.t('message.modified'));
}
return data?.code;
} catch (error) {
message.error('error');
}
},
});
return { setDocumentMeta: mutateAsync, data, loading };
};
export const useCreateDocument = () => {
const { id } = useParams();
const { setPaginationParams, page } = useSetPaginationParams();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.CreateDocument],
mutationFn: async (name: string) => {
const { data } = await kbService.document_create({
name,
kb_id: id,
});
if (data.code === 0) {
if (page === 1) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
} else {
setPaginationParams(); // fetch document list
}
message.success(i18n.t('message.created'));
}
return data.code;
},
});
return { createDocument: mutateAsync, loading, data };
};

View File

@@ -0,0 +1,231 @@
import message from '@/components/ui/message';
import {
IFetchFileListResult,
IFolder,
} from '@/interfaces/database/file-manager';
import fileManagerService from '@/services/file-manager-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { PaginationProps } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { useSetPaginationParams } from './route-hook';
export const enum FileApiAction {
UploadFile = 'uploadFile',
FetchFileList = 'fetchFileList',
MoveFile = 'moveFile',
CreateFolder = 'createFolder',
FetchParentFolderList = 'fetchParentFolderList',
DeleteFile = 'deleteFile',
}
export const useGetFolderId = () => {
const [searchParams] = useSearchParams();
const id = searchParams.get('folderId') as string;
return id ?? '';
};
export const useUploadFile = () => {
const { setPaginationParams } = useSetPaginationParams();
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [FileApiAction.UploadFile],
mutationFn: async (params: { fileList: File[]; parentId: string }) => {
const fileList = params.fileList;
const pathList = params.fileList.map(
(file) => (file as any).webkitRelativePath,
);
const formData = new FormData();
formData.append('parent_id', params.parentId);
fileList.forEach((file: any, index: number) => {
formData.append('file', file);
formData.append('path', pathList[index]);
});
try {
const ret = await fileManagerService.uploadFile(formData);
if (ret?.data.code === 0) {
message.success(t('message.uploaded'));
setPaginationParams(1);
queryClient.invalidateQueries({
queryKey: [FileApiAction.FetchFileList],
});
}
return ret?.data?.code;
} catch (error) {}
},
});
return { data, loading, uploadFile: mutateAsync };
};
export interface IMoveFileBody {
src_file_ids: string[];
dest_file_id: string; // target folder id
}
export const useMoveFile = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [FileApiAction.MoveFile],
mutationFn: async (params: IMoveFileBody) => {
const { data } = await fileManagerService.moveFile(params);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({
queryKey: [FileApiAction.FetchFileList],
});
}
return data.code;
},
});
return { data, loading, moveFile: mutateAsync };
};
export const useCreateFolder = () => {
const { setPaginationParams } = useSetPaginationParams();
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [FileApiAction.CreateFolder],
mutationFn: async (params: { parentId: string; name: string }) => {
const { data } = await fileManagerService.createFolder({
...params,
type: 'folder',
});
if (data.code === 0) {
message.success(t('message.created'));
setPaginationParams(1);
queryClient.invalidateQueries({
queryKey: [FileApiAction.FetchFileList],
});
}
return data.code;
},
});
return { data, loading, createFolder: mutateAsync };
};
export const useFetchParentFolderList = () => {
const id = useGetFolderId();
const { data } = useQuery<IFolder[]>({
queryKey: [FileApiAction.FetchParentFolderList, id],
initialData: [],
enabled: !!id,
queryFn: async () => {
const { data } = await fileManagerService.getAllParentFolder({
fileId: id,
});
return data?.data?.parent_folders?.toReversed() ?? [];
},
});
return data;
};
export interface IListResult {
searchString: string;
handleInputChange: React.ChangeEventHandler<HTMLInputElement>;
pagination: PaginationProps;
setPagination: (pagination: { page: number; pageSize: number }) => void;
loading: boolean;
}
export const useFetchFileList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const id = useGetFolderId();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { data, isFetching: loading } = useQuery<IFetchFileListResult>({
queryKey: [
FileApiAction.FetchFileList,
{
id,
debouncedSearchString,
...pagination,
},
],
initialData: { files: [], parent_folder: {} as IFolder, total: 0 },
gcTime: 0,
queryFn: async () => {
const { data } = await fileManagerService.listFile({
parent_id: id,
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
});
return data?.data;
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);
return {
...data,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
setPagination,
loading,
};
};
export const useDeleteFile = () => {
const { setPaginationParams } = useSetPaginationParams();
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [FileApiAction.DeleteFile],
mutationFn: async (params: { fileIds: string[]; parentId: string }) => {
const { data } = await fileManagerService.removeFile(params);
if (data.code === 0) {
message.success(t('message.deleted'));
setPaginationParams(1); // TODO: There should be a better way to paginate the request list
queryClient.invalidateQueries({
queryKey: [FileApiAction.FetchFileList],
});
}
return data.code;
},
});
return { data, loading, deleteFile: mutateAsync };
};

View File

@@ -0,0 +1,356 @@
import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-filter-submit';
import message from '@/components/ui/message';
import {
IKnowledge,
IKnowledgeGraph,
IKnowledgeResult,
INextTestingResult,
} from '@/interfaces/database/knowledge';
import { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
import i18n from '@/locales/config';
import kbService, {
deleteKnowledgeGraph,
getKnowledgeGraph,
listDataset,
} from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
export const enum KnowledgeApiAction {
TestRetrieval = 'testRetrieval',
FetchKnowledgeListByPage = 'fetchKnowledgeListByPage',
CreateKnowledge = 'createKnowledge',
DeleteKnowledge = 'deleteKnowledge',
SaveKnowledge = 'saveKnowledge',
FetchKnowledgeDetail = 'fetchKnowledgeDetail',
FetchKnowledgeGraph = 'fetchKnowledgeGraph',
FetchMetadata = 'fetchMetadata',
FetchKnowledgeList = 'fetchKnowledgeList',
RemoveKnowledgeGraph = 'removeKnowledgeGraph',
}
export const useKnowledgeBaseId = (): string => {
const { id } = useParams();
return (id as string) || '';
};
export const useTestRetrieval = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const [values, setValues] = useState<ITestRetrievalRequestBody>();
const mountedRef = useRef(false);
const { filterValue, handleFilterSubmit } = useHandleFilterSubmit();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const onPaginationChange = useCallback((page: number, pageSize: number) => {
setPage(page);
setPageSize(pageSize);
}, []);
const queryParams = useMemo(() => {
return {
...values,
kb_id: values?.kb_id || knowledgeBaseId,
page,
size: pageSize,
doc_ids: filterValue.doc_ids,
};
}, [filterValue, knowledgeBaseId, page, pageSize, values]);
const {
data,
isFetching: loading,
refetch,
} = useQuery<INextTestingResult>({
queryKey: [KnowledgeApiAction.TestRetrieval, queryParams, page, pageSize],
initialData: {
chunks: [],
doc_aggs: [],
total: 0,
isRuned: false,
},
enabled: false,
gcTime: 0,
queryFn: async () => {
const { data } = await kbService.retrieval_test(queryParams);
const result = data?.data ?? {};
return { ...result, isRuned: true };
},
});
useEffect(() => {
if (mountedRef.current) {
refetch();
}
mountedRef.current = true;
}, [page, pageSize, refetch, filterValue]);
return {
data,
loading,
setValues,
refetch,
onPaginationChange,
page,
pageSize,
handleFilterSubmit,
filterValue,
};
};
export const useFetchNextKnowledgeListByPage = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { filterValue, handleFilterSubmit } = useHandleFilterSubmit();
const { data, isFetching: loading } = useQuery<IKnowledgeResult>({
queryKey: [
KnowledgeApiAction.FetchKnowledgeListByPage,
{
debouncedSearchString,
...pagination,
filterValue,
},
],
initialData: {
kbs: [],
total: 0,
},
gcTime: 0,
queryFn: async () => {
const { data } = await listDataset(
{
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
{
owner_ids: filterValue.owner,
},
);
return data?.data;
},
});
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
// setPagination({ page: 1 }); // TODO: This results in repeated requests
handleInputChange(e);
},
[handleInputChange],
);
return {
...data,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
setPagination,
loading,
filterValue,
handleFilterSubmit,
};
};
export const useCreateKnowledge = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [KnowledgeApiAction.CreateKnowledge],
mutationFn: async (params: { id?: string; name: string }) => {
const { data = {} } = await kbService.createKb(params);
if (data.code === 0) {
message.success(
i18n.t(`message.${params?.id ? 'modified' : 'created'}`),
);
queryClient.invalidateQueries({ queryKey: ['fetchKnowledgeList'] });
}
return data;
},
});
return { data, loading, createKnowledge: mutateAsync };
};
export const useDeleteKnowledge = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [KnowledgeApiAction.DeleteKnowledge],
mutationFn: async (id: string) => {
const { data } = await kbService.rmKb({ kb_id: id });
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: [KnowledgeApiAction.FetchKnowledgeListByPage],
});
}
return data?.data ?? [];
},
});
return { data, loading, deleteKnowledge: mutateAsync };
};
export const useUpdateKnowledge = (shouldFetchList = false) => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [KnowledgeApiAction.SaveKnowledge],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await kbService.updateKb({
kb_id: params?.kb_id ? params?.kb_id : knowledgeBaseId,
...params,
});
if (data.code === 0) {
message.success(i18n.t(`message.updated`));
if (shouldFetchList) {
queryClient.invalidateQueries({
queryKey: [KnowledgeApiAction.FetchKnowledgeListByPage],
});
} else {
queryClient.invalidateQueries({ queryKey: ['fetchKnowledgeDetail'] });
}
}
return data;
},
});
return { data, loading, saveKnowledgeConfiguration: mutateAsync };
};
export const useFetchKnowledgeBaseConfiguration = (props?: {
isEdit?: boolean;
refreshCount?: number;
}) => {
const { isEdit = true, refreshCount } = props || { isEdit: true };
const { id } = useParams();
const [searchParams] = useSearchParams();
const knowledgeBaseId = searchParams.get('id') || id;
let queryKey: (KnowledgeApiAction | number)[] = [
KnowledgeApiAction.FetchKnowledgeDetail,
];
if (typeof refreshCount === 'number') {
queryKey = [KnowledgeApiAction.FetchKnowledgeDetail, refreshCount];
}
const { data, isFetching: loading } = useQuery<IKnowledge>({
queryKey,
initialData: {} as IKnowledge,
gcTime: 0,
queryFn: async () => {
if (isEdit) {
const { data } = await kbService.get_kb_detail({
kb_id: knowledgeBaseId,
});
return data?.data ?? {};
} else {
return {};
}
},
});
return { data, loading };
};
export function useFetchKnowledgeGraph() {
const knowledgeBaseId = useKnowledgeBaseId();
const { data, isFetching: loading } = useQuery<IKnowledgeGraph>({
queryKey: [KnowledgeApiAction.FetchKnowledgeGraph, knowledgeBaseId],
initialData: { graph: {}, mind_map: {} } as IKnowledgeGraph,
enabled: !!knowledgeBaseId,
gcTime: 0,
queryFn: async () => {
const { data } = await getKnowledgeGraph(knowledgeBaseId);
return data?.data;
},
});
return { data, loading };
}
export function useFetchKnowledgeMetadata(kbIds: string[] = []) {
const { data, isFetching: loading } = useQuery<
Record<string, Record<string, string[]>>
>({
queryKey: [KnowledgeApiAction.FetchMetadata, kbIds],
initialData: {},
enabled: kbIds.length > 0,
gcTime: 0,
queryFn: async () => {
const { data } = await kbService.getMeta({ kb_ids: kbIds.join(',') });
return data?.data ?? {};
},
});
return { data, loading };
}
export const useRemoveKnowledgeGraph = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [KnowledgeApiAction.RemoveKnowledgeGraph],
mutationFn: async () => {
const { data } = await deleteKnowledgeGraph(knowledgeBaseId);
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: [KnowledgeApiAction.FetchKnowledgeGraph],
});
}
return data?.code;
},
});
return { data, loading, removeKnowledgeGraph: mutateAsync };
};
export const useFetchKnowledgeList = (
shouldFilterListWithoutDocument: boolean = false,
): {
list: IKnowledge[];
loading: boolean;
} => {
const { data, isFetching: loading } = useQuery({
queryKey: [KnowledgeApiAction.FetchKnowledgeList],
initialData: [],
gcTime: 0, // https://tanstack.com/query/latest/docs/framework/react/guides/caching?from=reactQueryV3
queryFn: async () => {
const { data } = await listDataset();
const list = data?.data?.kbs ?? [];
return shouldFilterListWithoutDocument
? list.filter((x: IKnowledge) => x.chunk_num > 0)
: list;
},
});
return { list: data, loading };
};

View File

@@ -0,0 +1,47 @@
import { LlmModelType } from '@/constants/knowledge';
import userService from '@/services/user-service';
import { useQuery } from '@tanstack/react-query';
import {
IThirdOAIModelCollection as IThirdAiModelCollection,
IThirdOAIModel,
} from '@/interfaces/database/llm';
import { buildLlmUuid } from '@/utils/llm-util';
export const enum LLMApiAction {
LlmList = 'llmList',
}
export const useFetchLlmList = (modelType?: LlmModelType) => {
const { data } = useQuery<IThirdAiModelCollection>({
queryKey: [LLMApiAction.LlmList],
initialData: {},
queryFn: async () => {
const { data } = await userService.llm_list({ model_type: modelType });
return data?.data ?? {};
},
});
return data;
};
type IThirdOAIModelWithUuid = IThirdOAIModel & { uuid: string };
export function useSelectFlatLlmList(modelType?: LlmModelType) {
const llmList = useFetchLlmList(modelType);
return Object.values(llmList).reduce<IThirdOAIModelWithUuid[]>((pre, cur) => {
pre.push(...cur.map((x) => ({ ...x, uuid: buildLlmUuid(x) })));
return pre;
}, []);
}
export function useFindLlmByUuid(modelType?: LlmModelType) {
const flatList = useSelectFlatLlmList(modelType);
return (uuid: string) => {
return flatList.find((x) => x.uuid === uuid);
};
}

View File

@@ -0,0 +1,270 @@
import message from '@/components/ui/message';
import { ResponseType } from '@/interfaces/database/base';
import {
IExportedMcpServers,
IMcpServer,
IMcpServerListResponse,
IMCPTool,
IMCPToolRecord,
} from '@/interfaces/database/mcp';
import {
IImportMcpServersRequestBody,
ITestMcpRequestBody,
} from '@/interfaces/request/mcp';
import i18n from '@/locales/config';
import mcpServerService, {
listMcpServers,
} from '@/services/mcp-server-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useState } from 'react';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
export const enum McpApiAction {
ListMcpServer = 'listMcpServer',
GetMcpServer = 'getMcpServer',
CreateMcpServer = 'createMcpServer',
UpdateMcpServer = 'updateMcpServer',
DeleteMcpServer = 'deleteMcpServer',
ImportMcpServer = 'importMcpServer',
ExportMcpServer = 'exportMcpServer',
ListMcpServerTools = 'listMcpServerTools',
TestMcpServerTool = 'testMcpServerTool',
CacheMcpServerTool = 'cacheMcpServerTool',
TestMcpServer = 'testMcpServer',
}
export const useListMcpServer = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { data, isFetching: loading } = useQuery<IMcpServerListResponse>({
queryKey: [
McpApiAction.ListMcpServer,
{
debouncedSearchString,
...pagination,
},
],
initialData: { total: 0, mcp_servers: [] },
gcTime: 0,
queryFn: async () => {
const { data } = await listMcpServers({
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
});
return data?.data;
},
});
return {
data,
loading,
handleInputChange,
setPagination,
searchString,
pagination: { ...pagination, total: data?.total },
};
};
export const useGetMcpServer = (id: string) => {
const { data, isFetching: loading } = useQuery<IMcpServer>({
queryKey: [McpApiAction.GetMcpServer, id],
initialData: {} as IMcpServer,
gcTime: 0,
enabled: !!id,
queryFn: async () => {
const { data } = await mcpServerService.get({ mcp_id: id });
return data?.data ?? {};
},
});
return { data, loading, id };
};
export const useCreateMcpServer = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [McpApiAction.CreateMcpServer],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await mcpServerService.create(params);
if (data.code === 0) {
message.success(i18n.t(`message.created`));
queryClient.invalidateQueries({
queryKey: [McpApiAction.ListMcpServer],
});
}
return data.code;
},
});
return { data, loading, createMcpServer: mutateAsync };
};
export const useUpdateMcpServer = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [McpApiAction.UpdateMcpServer],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await mcpServerService.update(params);
if (data.code === 0) {
message.success(i18n.t(`message.updated`));
queryClient.invalidateQueries({
queryKey: [McpApiAction.ListMcpServer],
});
}
return data.code;
},
});
return { data, loading, updateMcpServer: mutateAsync };
};
export const useDeleteMcpServer = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [McpApiAction.DeleteMcpServer],
mutationFn: async (ids: string[]) => {
const { data = {} } = await mcpServerService.delete({ mcp_ids: ids });
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: [McpApiAction.ListMcpServer],
});
}
return data;
},
});
return { data, loading, deleteMcpServer: mutateAsync };
};
export const useImportMcpServer = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [McpApiAction.ImportMcpServer],
mutationFn: async (params: IImportMcpServersRequestBody) => {
const { data = {} } = await mcpServerService.import(params);
if (data.code === 0) {
message.success(i18n.t(`message.operated`));
queryClient.invalidateQueries({
queryKey: [McpApiAction.ListMcpServer],
});
}
return data;
},
});
return { data, loading, importMcpServer: mutateAsync };
};
export const useExportMcpServer = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation<ResponseType<IExportedMcpServers>, Error, string[]>({
mutationKey: [McpApiAction.ExportMcpServer],
mutationFn: async (ids) => {
const { data = {} } = await mcpServerService.export({ mcp_ids: ids });
if (data.code === 0) {
message.success(i18n.t(`message.operated`));
}
return data;
},
});
return { data, loading, exportMcpServer: mutateAsync };
};
export const useListMcpServerTools = () => {
const [ids, setIds] = useState<string[]>([]);
const { data, isFetching: loading } = useQuery<IMCPToolRecord>({
queryKey: [McpApiAction.ListMcpServerTools],
initialData: {} as IMCPToolRecord,
gcTime: 0,
enabled: ids.length > 0,
queryFn: async () => {
const { data } = await mcpServerService.listTools({ mcp_ids: ids });
return data?.data ?? {};
},
});
return { data, loading, setIds };
};
export const useTestMcpServer = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation<ResponseType<IMCPTool[]>, Error, ITestMcpRequestBody>({
mutationKey: [McpApiAction.TestMcpServer],
mutationFn: async (params) => {
const { data } = await mcpServerService.test(params);
return data;
},
});
return { data, loading, testMcpServer: mutateAsync };
};
export const useCacheMcpServerTool = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [McpApiAction.CacheMcpServerTool],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await mcpServerService.cacheTool(params);
return data;
},
});
return { data, loading, cacheMcpServerTool: mutateAsync };
};
export const useTestMcpServerTool = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [McpApiAction.TestMcpServerTool],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await mcpServerService.testTool(params);
return data;
},
});
return { data, loading, testMcpServerTool: mutateAsync };
};

View File

@@ -0,0 +1,188 @@
import message from '@/components/ui/message';
import { Authorization } from '@/constants/authorization';
import { IReferenceObject } from '@/interfaces/database/chat';
import { BeginQuery } from '@/pages/agent/interface';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { EventSourceParserStream } from 'eventsource-parser/stream';
import { useCallback, useRef, useState } from 'react';
export enum MessageEventType {
WorkflowStarted = 'workflow_started',
NodeStarted = 'node_started',
NodeFinished = 'node_finished',
Message = 'message',
MessageEnd = 'message_end',
WorkflowFinished = 'workflow_finished',
UserInputs = 'user_inputs',
NodeLogs = 'node_logs',
}
export interface IAnswerEvent<T> {
event: MessageEventType;
message_id: string;
session_id: string;
created_at: number;
task_id: string;
data: T;
}
export interface INodeData {
inputs: Record<string, any>;
outputs: Record<string, any>;
component_id: string;
component_name: string;
component_type: string;
error: null | string;
elapsed_time: number;
created_at: number;
thoughts: string;
}
export interface IInputData {
content: string;
inputs: Record<string, BeginQuery>;
tips: string;
}
export interface IMessageData {
content: string;
start_to_think?: boolean;
end_to_think?: boolean;
}
export interface IMessageEndData {
reference: IReferenceObject;
}
export interface ILogData extends INodeData {
logs: {
name: string;
result: string;
args: {
query: string;
topic: string;
};
};
}
export type INodeEvent = IAnswerEvent<INodeData>;
export type IMessageEvent = IAnswerEvent<IMessageData>;
export type IMessageEndEvent = IAnswerEvent<IMessageEndData>;
export type IInputEvent = IAnswerEvent<IInputData>;
export type ILogEvent = IAnswerEvent<ILogData>;
export type IChatEvent = INodeEvent | IMessageEvent | IMessageEndEvent;
export type IEventList = Array<IChatEvent>;
export const useSendMessageBySSE = (url: string = api.completeConversation) => {
const [answerList, setAnswerList] = useState<IEventList>([]);
const [done, setDone] = useState(true);
const timer = useRef<any>();
const sseRef = useRef<AbortController>();
const initializeSseRef = useCallback(() => {
sseRef.current = new AbortController();
}, []);
const resetAnswerList = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
setAnswerList([]);
clearTimeout(timer.current);
}, 1000);
}, []);
const send = useCallback(
async (
body: any,
controller?: AbortController,
): Promise<{ response: Response; data: ResponseType } | undefined> => {
initializeSseRef();
try {
setDone(false);
const response = await fetch(url, {
method: 'POST',
headers: {
[Authorization]: getAuthorization(),
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: controller?.signal || sseRef.current?.signal,
});
const res = response.clone().json();
const reader = response?.body
?.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
.getReader();
while (true) {
try {
const x = await reader?.read();
if (x) {
const { done, value } = x;
if (done) {
console.info('done');
resetAnswerList();
break;
}
try {
const val = JSON.parse(value?.data || '');
console.info('data:', val);
if (val.code === 500) {
message.error(val.message);
}
setAnswerList((list) => {
const nextList = [...list];
nextList.push(val);
return nextList;
});
} catch (e) {
console.warn(e);
}
}
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.log('Request was aborted by user or logic.');
break;
}
}
}
console.info('done?');
setDone(true);
resetAnswerList();
return { data: await res, response };
} catch (e) {
setDone(true);
resetAnswerList();
console.warn(e);
}
},
[initializeSseRef, url, resetAnswerList],
);
const stopOutputMessage = useCallback(() => {
sseRef.current?.abort();
}, []);
return {
send,
answerList,
done,
setDone,
resetAnswerList,
stopOutputMessage,
};
};

View File

@@ -0,0 +1,464 @@
import message from '@/components/ui/message';
import { LanguageTranslationMap } from '@/constants/common';
import { ResponseGetType } from '@/interfaces/database/base';
import { IToken } from '@/interfaces/database/chat';
import { ITenantInfo } from '@/interfaces/database/knowledge';
import { ILangfuseConfig } from '@/interfaces/database/system';
import {
ISystemStatus,
ITenant,
ITenantUser,
IUserInfo,
} from '@/interfaces/database/user-setting';
import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system';
import userService, {
addTenantUser,
agreeTenant,
deleteTenantUser,
listTenant,
listTenantUser,
} from '@/services/user-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Modal } from 'antd';
import DOMPurify from 'dompurify';
import { isEmpty } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { history } from 'umi';
export const enum UserSettingApiAction {
UserInfo = 'userInfo',
TenantInfo = 'tenantInfo',
SaveSetting = 'saveSetting',
FetchManualSystemTokenList = 'fetchManualSystemTokenList',
FetchSystemTokenList = 'fetchSystemTokenList',
RemoveSystemToken = 'removeSystemToken',
CreateSystemToken = 'createSystemToken',
ListTenantUser = 'listTenantUser',
AddTenantUser = 'addTenantUser',
DeleteTenantUser = 'deleteTenantUser',
ListTenant = 'listTenant',
AgreeTenant = 'agreeTenant',
SetLangfuseConfig = 'setLangfuseConfig',
DeleteLangfuseConfig = 'deleteLangfuseConfig',
FetchLangfuseConfig = 'fetchLangfuseConfig',
}
export const useFetchUserInfo = (): ResponseGetType<IUserInfo> => {
const { i18n } = useTranslation();
const { data, isFetching: loading } = useQuery({
queryKey: [UserSettingApiAction.UserInfo],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.user_info();
if (data.code === 0) {
i18n.changeLanguage(
LanguageTranslationMap[
data.data.language as keyof typeof LanguageTranslationMap
],
);
}
return data?.data ?? {};
},
});
return { data, loading };
};
export const useFetchTenantInfo = (
showEmptyModelWarn = false,
): ResponseGetType<ITenantInfo> => {
const { t } = useTranslation();
const { data, isFetching: loading } = useQuery({
queryKey: [UserSettingApiAction.TenantInfo],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data: res } = await userService.get_tenant_info();
if (res.code === 0) {
// llm_id is chat_id
// asr_id is speech2txt
const { data } = res;
if (
showEmptyModelWarn &&
(isEmpty(data.embd_id) || isEmpty(data.llm_id))
) {
Modal.warning({
title: t('common.warn'),
content: (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('setting.modelProvidersWarn')),
}}
></div>
),
onOk() {
history.push('/user-setting/model');
},
});
}
data.chat_id = data.llm_id;
data.speech2text_id = data.asr_id;
return data;
}
return res;
},
});
return { data, loading };
};
export const useSelectParserList = (): Array<{
value: string;
label: string;
}> => {
const { data: tenantInfo } = useFetchTenantInfo(true);
const parserList = useMemo(() => {
const parserArray: Array<string> = tenantInfo?.parser_ids?.split(',') ?? [];
return parserArray.map((x) => {
const arr = x.split(':');
return { value: arr[0], label: arr[1] };
});
}, [tenantInfo]);
return parserList;
};
export const useSaveSetting = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.SaveSetting],
mutationFn: async (
userInfo: { new_password: string } | Partial<IUserInfo>,
) => {
const { data } = await userService.setting(userInfo);
if (data.code === 0) {
message.success(t('message.modified'));
queryClient.invalidateQueries({ queryKey: ['userInfo'] });
}
return data?.code;
},
});
return { data, loading, saveSetting: mutateAsync };
};
export const useFetchSystemVersion = () => {
const [version, setVersion] = useState('');
const [loading, setLoading] = useState(false);
const fetchSystemVersion = useCallback(async () => {
try {
setLoading(true);
const { data } = await userService.getSystemVersion();
if (data.code === 0) {
setVersion(data.data);
setLoading(false);
}
} catch (error) {
setLoading(false);
}
}, []);
return { fetchSystemVersion, version, loading };
};
export const useFetchSystemStatus = () => {
const [systemStatus, setSystemStatus] = useState<ISystemStatus>(
{} as ISystemStatus,
);
const [loading, setLoading] = useState(false);
const fetchSystemStatus = useCallback(async () => {
setLoading(true);
const { data } = await userService.getSystemStatus();
if (data.code === 0) {
setSystemStatus(data.data);
setLoading(false);
}
}, []);
return {
systemStatus,
fetchSystemStatus,
loading,
};
};
export const useFetchManualSystemTokenList = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.FetchManualSystemTokenList],
mutationFn: async () => {
const { data } = await userService.listToken();
return data?.data ?? [];
},
});
return { data, loading, fetchSystemTokenList: mutateAsync };
};
export const useFetchSystemTokenList = () => {
const {
data,
isFetching: loading,
refetch,
} = useQuery<IToken[]>({
queryKey: [UserSettingApiAction.FetchSystemTokenList],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await userService.listToken();
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useRemoveSystemToken = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.RemoveSystemToken],
mutationFn: async (token: string) => {
const { data } = await userService.removeToken({}, token);
if (data.code === 0) {
message.success(t('message.deleted'));
queryClient.invalidateQueries({
queryKey: [UserSettingApiAction.FetchSystemTokenList],
});
}
return data?.data ?? [];
},
});
return { data, loading, removeToken: mutateAsync };
};
export const useCreateSystemToken = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.CreateSystemToken],
mutationFn: async (params: Record<string, any>) => {
const { data } = await userService.createToken(params);
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [UserSettingApiAction.FetchSystemTokenList],
});
}
return data?.data ?? [];
},
});
return { data, loading, createToken: mutateAsync };
};
export const useListTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITenantUser[]>({
queryKey: [UserSettingApiAction.ListTenantUser, tenantId],
initialData: [],
gcTime: 0,
enabled: !!tenantId,
queryFn: async () => {
const { data } = await listTenantUser(tenantId);
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useAddTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.AddTenantUser],
mutationFn: async (email: string) => {
const { data } = await addTenantUser(tenantInfo.tenant_id, email);
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [UserSettingApiAction.ListTenantUser],
});
}
return data?.code;
},
});
return { data, loading, addTenantUser: mutateAsync };
};
export const useDeleteTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.DeleteTenantUser],
mutationFn: async ({
userId,
tenantId,
}: {
userId: string;
tenantId?: string;
}) => {
const { data } = await deleteTenantUser({
tenantId: tenantId ?? tenantInfo.tenant_id,
userId,
});
if (data.code === 0) {
message.success(t('message.deleted'));
queryClient.invalidateQueries({
queryKey: [UserSettingApiAction.ListTenantUser],
});
queryClient.invalidateQueries({
queryKey: [UserSettingApiAction.ListTenant],
});
}
return data?.data ?? [];
},
});
return { data, loading, deleteTenantUser: mutateAsync };
};
export const useListTenant = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITenant[]>({
queryKey: [UserSettingApiAction.ListTenant, tenantId],
initialData: [],
gcTime: 0,
enabled: !!tenantId,
queryFn: async () => {
const { data } = await listTenant();
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useAgreeTenant = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.AgreeTenant],
mutationFn: async (tenantId: string) => {
const { data } = await agreeTenant(tenantId);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({
queryKey: [UserSettingApiAction.ListTenant],
});
}
return data?.data ?? [];
},
});
return { data, loading, agreeTenant: mutateAsync };
};
export const useSetLangfuseConfig = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.SetLangfuseConfig],
mutationFn: async (params: ISetLangfuseConfigRequestBody) => {
const { data } = await userService.setLangfuseConfig(params);
if (data.code === 0) {
message.success(t('message.operated'));
}
return data?.code;
},
});
return { data, loading, setLangfuseConfig: mutateAsync };
};
export const useDeleteLangfuseConfig = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [UserSettingApiAction.DeleteLangfuseConfig],
mutationFn: async () => {
const { data } = await userService.deleteLangfuseConfig();
if (data.code === 0) {
message.success(t('message.deleted'));
}
return data?.code;
},
});
return { data, loading, deleteLangfuseConfig: mutateAsync };
};
export const useFetchLangfuseConfig = () => {
const { data, isFetching: loading } = useQuery<ILangfuseConfig>({
queryKey: [UserSettingApiAction.FetchLangfuseConfig],
gcTime: 0,
queryFn: async () => {
const { data } = await userService.getLangfuseConfig();
return data?.data;
},
});
return { data, loading };
};

View File

@@ -0,0 +1,434 @@
import message from '@/components/ui/message';
import { LanguageTranslationMap } from '@/constants/common';
import { ResponseGetType } from '@/interfaces/database/base';
import { IToken } from '@/interfaces/database/chat';
import { ITenantInfo } from '@/interfaces/database/knowledge';
import { ILangfuseConfig } from '@/interfaces/database/system';
import {
ISystemStatus,
ITenant,
ITenantUser,
IUserInfo,
} from '@/interfaces/database/user-setting';
import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system';
import userService, {
addTenantUser,
agreeTenant,
deleteTenantUser,
listTenant,
listTenantUser,
} from '@/services/user-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Modal } from 'antd';
import DOMPurify from 'dompurify';
import { isEmpty } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { history } from 'umi';
export const useFetchUserInfo = (): ResponseGetType<IUserInfo> => {
const { i18n } = useTranslation();
const { data, isFetching: loading } = useQuery({
queryKey: ['userInfo'],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.user_info();
if (data.code === 0) {
i18n.changeLanguage(
LanguageTranslationMap[
data.data.language as keyof typeof LanguageTranslationMap
],
);
}
return data?.data ?? {};
},
});
return { data, loading };
};
export const useFetchTenantInfo = (
showEmptyModelWarn = false,
): ResponseGetType<ITenantInfo> => {
const { t } = useTranslation();
const { data, isFetching: loading } = useQuery({
queryKey: ['tenantInfo'],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data: res } = await userService.get_tenant_info();
if (res.code === 0) {
// llm_id is chat_id
// asr_id is speech2txt
const { data } = res;
if (
showEmptyModelWarn &&
(isEmpty(data.embd_id) || isEmpty(data.llm_id))
) {
Modal.warning({
title: t('common.warn'),
content: (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('setting.modelProvidersWarn')),
}}
></div>
),
onOk() {
history.push('/user-setting/model');
},
});
}
data.chat_id = data.llm_id;
data.speech2text_id = data.asr_id;
return data;
}
return res;
},
});
return { data, loading };
};
export const useSelectParserList = (): Array<{
value: string;
label: string;
}> => {
const { data: tenantInfo } = useFetchTenantInfo(true);
const parserList = useMemo(() => {
const parserArray: Array<string> = tenantInfo?.parser_ids?.split(',') ?? [];
return parserArray.map((x) => {
const arr = x.split(':');
return { value: arr[0], label: arr[1] };
});
}, [tenantInfo]);
return parserList;
};
export const useSaveSetting = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveSetting'],
mutationFn: async (
userInfo: { new_password: string } | Partial<IUserInfo>,
) => {
const { data } = await userService.setting(userInfo);
if (data.code === 0) {
message.success(t('message.modified'));
queryClient.invalidateQueries({ queryKey: ['userInfo'] });
}
return data?.code;
},
});
return { data, loading, saveSetting: mutateAsync };
};
export const useFetchSystemVersion = () => {
const [version, setVersion] = useState('');
const [loading, setLoading] = useState(false);
const fetchSystemVersion = useCallback(async () => {
try {
setLoading(true);
const { data } = await userService.getSystemVersion();
if (data.code === 0) {
setVersion(data.data);
setLoading(false);
}
} catch (error) {
setLoading(false);
}
}, []);
return { fetchSystemVersion, version, loading };
};
export const useFetchSystemStatus = () => {
const [systemStatus, setSystemStatus] = useState<ISystemStatus>(
{} as ISystemStatus,
);
const [loading, setLoading] = useState(false);
const fetchSystemStatus = useCallback(async () => {
setLoading(true);
const { data } = await userService.getSystemStatus();
if (data.code === 0) {
setSystemStatus(data.data);
setLoading(false);
}
}, []);
return {
systemStatus,
fetchSystemStatus,
loading,
};
};
export const useFetchManualSystemTokenList = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['fetchManualSystemTokenList'],
mutationFn: async () => {
const { data } = await userService.listToken();
return data?.data ?? [];
},
});
return { data, loading, fetchSystemTokenList: mutateAsync };
};
export const useFetchSystemTokenList = () => {
const {
data,
isFetching: loading,
refetch,
} = useQuery<IToken[]>({
queryKey: ['fetchSystemTokenList'],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await userService.listToken();
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useRemoveSystemToken = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['removeSystemToken'],
mutationFn: async (token: string) => {
const { data } = await userService.removeToken({}, token);
if (data.code === 0) {
message.success(t('message.deleted'));
queryClient.invalidateQueries({ queryKey: ['fetchSystemTokenList'] });
}
return data?.data ?? [];
},
});
return { data, loading, removeToken: mutateAsync };
};
export const useCreateSystemToken = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createSystemToken'],
mutationFn: async (params: Record<string, any>) => {
const { data } = await userService.createToken(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchSystemTokenList'] });
}
return data?.data ?? [];
},
});
return { data, loading, createToken: mutateAsync };
};
export const useListTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITenantUser[]>({
queryKey: ['listTenantUser', tenantId],
initialData: [],
gcTime: 0,
enabled: !!tenantId,
queryFn: async () => {
const { data } = await listTenantUser(tenantId);
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useAddTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['addTenantUser'],
mutationFn: async (email: string) => {
const { data } = await addTenantUser(tenantInfo.tenant_id, email);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['listTenantUser'] });
}
return data?.code;
},
});
return { data, loading, addTenantUser: mutateAsync };
};
export const useDeleteTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteTenantUser'],
mutationFn: async ({
userId,
tenantId,
}: {
userId: string;
tenantId?: string;
}) => {
const { data } = await deleteTenantUser({
tenantId: tenantId ?? tenantInfo.tenant_id,
userId,
});
if (data.code === 0) {
message.success(t('message.deleted'));
queryClient.invalidateQueries({ queryKey: ['listTenantUser'] });
queryClient.invalidateQueries({ queryKey: ['listTenant'] });
}
return data?.data ?? [];
},
});
return { data, loading, deleteTenantUser: mutateAsync };
};
export const useListTenant = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITenant[]>({
queryKey: ['listTenant', tenantId],
initialData: [],
gcTime: 0,
enabled: !!tenantId,
queryFn: async () => {
const { data } = await listTenant();
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useAgreeTenant = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['agreeTenant'],
mutationFn: async (tenantId: string) => {
const { data } = await agreeTenant(tenantId);
if (data.code === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({ queryKey: ['listTenant'] });
}
return data?.data ?? [];
},
});
return { data, loading, agreeTenant: mutateAsync };
};
export const useSetLangfuseConfig = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['setLangfuseConfig'],
mutationFn: async (params: ISetLangfuseConfigRequestBody) => {
const { data } = await userService.setLangfuseConfig(params);
if (data.code === 0) {
message.success(t('message.operated'));
}
return data?.code;
},
});
return { data, loading, setLangfuseConfig: mutateAsync };
};
export const useDeleteLangfuseConfig = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteLangfuseConfig'],
mutationFn: async () => {
const { data } = await userService.deleteLangfuseConfig();
if (data.code === 0) {
message.success(t('message.deleted'));
}
return data?.code;
},
});
return { data, loading, deleteLangfuseConfig: mutateAsync };
};
export const useFetchLangfuseConfig = () => {
const { data, isFetching: loading } = useQuery<ILangfuseConfig>({
queryKey: ['fetchLangfuseConfig'],
gcTime: 0,
queryFn: async () => {
const { data } = await userService.getLangfuseConfig();
return data?.data;
},
});
return { data, loading };
};