mirror of https://github.com/buster-so/buster.git
chat update
This commit is contained in:
parent
7a349da981
commit
644d7f89d8
|
@ -5,9 +5,5 @@ export default function Page({
|
||||||
}: {
|
}: {
|
||||||
params: { chatId: string; messageId: string };
|
params: { chatId: string; messageId: string };
|
||||||
}) {
|
}) {
|
||||||
return (
|
return <ReasoningController chatId={chatId} messageId={messageId} />;
|
||||||
<>
|
|
||||||
<ReasoningController chatId={chatId} messageId={messageId} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { Text } from '@/components';
|
import { Text } from '@/components/text';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const VersionPill: React.FC<{ version_number: number }> = React.memo(
|
export const VersionPill: React.FC<{ version_number: number }> = React.memo(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { useChatIndividualContextSelector } from '../../_layouts/ChatLayout/ChatContext';
|
import { useChatIndividualContextSelector } from '../../_layouts/ChatLayout/ChatContext';
|
||||||
import { ReasoningMessageContainer } from './ReasoningMessageContainer';
|
import { ReasoningMessageContainer } from './ReasoningMessageContainer';
|
||||||
import { useBusterChatContextSelector } from '@/context/Chats';
|
import { useBusterChatContextSelector } from '@/context/Chats';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { FileType } from '@/api/asset_interfaces';
|
import type { FileType } from '@/api/asset_interfaces';
|
||||||
import { createChatAssetRoute } from '@appLayouts/ChatLayout/ChatLayoutContext/helpers';
|
import { createChatAssetRoute } from '@appLayouts/ChatLayout/ChatLayoutContext/helpers';
|
||||||
import { AppMaterialIcons, AppTooltip } from '@/components';
|
import { AppTooltip } from '@/components/tooltip';
|
||||||
|
import { AppMaterialIcons } from '@/components/icons';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useChatLayoutContextSelector } from '@/app/app/_layouts/ChatLayout';
|
import { useChatLayoutContextSelector } from '@/app/app/_layouts/ChatLayout';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text } from '@/components';
|
import { Text } from '@/components/text';
|
||||||
import { VersionPill } from '@/app/app/_components/Text/VersionPill';
|
import { VersionPill } from '@appComponents/Text/VersionPill';
|
||||||
|
|
||||||
export const ReasoningFileTitle = React.memo(
|
export const ReasoningFileTitle = React.memo(
|
||||||
({ file_name, version_number }: { file_name: string; version_number: number }) => {
|
({ file_name, version_number }: { file_name: string; version_number: number }) => {
|
||||||
|
|
|
@ -13,14 +13,11 @@ const autoSize = { minRows: 3, maxRows: 16 };
|
||||||
|
|
||||||
export const ChatInput: React.FC<{}> = React.memo(({}) => {
|
export const ChatInput: React.FC<{}> = React.memo(({}) => {
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
const isNewChat = useChatIndividualContextSelector((x) => x.isNewChat);
|
|
||||||
const isFollowUpChat = useChatIndividualContextSelector((x) => x.isFollowUpChat);
|
|
||||||
const inputRef = useRef<TextAreaRef>(null);
|
const inputRef = useRef<TextAreaRef>(null);
|
||||||
|
|
||||||
|
const loading = useChatIndividualContextSelector((x) => x.isLoading);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isFocused, setIsFocused] = React.useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const loading = isNewChat || isFollowUpChat;
|
|
||||||
|
|
||||||
const disableSendButton = useMemo(() => {
|
const disableSendButton = useMemo(() => {
|
||||||
return !inputHasText(inputValue);
|
return !inputHasText(inputValue);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import type { SelectedFile } from '../interfaces';
|
import type { SelectedFile } from '../interfaces';
|
||||||
import { useSubscribeIndividualChat } from './useSubscribeIndividualChat';
|
import { useSubscribeIndividualChat } from './useSubscribeIndividualChat';
|
||||||
import { useAutoChangeLayout } from './useAutoChangeLayout';
|
import { useAutoChangeLayout } from './useAutoChangeLayout';
|
||||||
|
import { useBusterChatContextSelector } from '@/context/Chats';
|
||||||
|
|
||||||
export const useChatIndividualContext = ({
|
export const useChatIndividualContext = ({
|
||||||
chatId,
|
chatId,
|
||||||
|
@ -25,6 +26,7 @@ export const useChatIndividualContext = ({
|
||||||
chatId,
|
chatId,
|
||||||
defaultSelectedFile
|
defaultSelectedFile
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasChat = !!chatId && !!chat;
|
const hasChat = !!chatId && !!chat;
|
||||||
const chatTitle = chat?.title;
|
const chatTitle = chat?.title;
|
||||||
const chatMessageIds = chat?.messages ?? [];
|
const chatMessageIds = chat?.messages ?? [];
|
||||||
|
@ -34,10 +36,11 @@ export const useChatIndividualContext = ({
|
||||||
|
|
||||||
//MESSAGES
|
//MESSAGES
|
||||||
const currentMessageId = chatMessageIds[chatMessageIds.length - 1];
|
const currentMessageId = chatMessageIds[chatMessageIds.length - 1];
|
||||||
const isNewChat = chat?.isNewChat ?? false;
|
const isLoading = useBusterChatContextSelector(
|
||||||
const isFollowUpChat = chat?.isFollowupMessage ?? false;
|
(x) => x.chatsMessages[currentMessageId]?.isCompletedStream
|
||||||
|
);
|
||||||
|
|
||||||
useAutoChangeLayout({ lastMessageId: currentMessageId, onSetSelectedFile, chat });
|
useAutoChangeLayout({ lastMessageId: currentMessageId, onSetSelectedFile });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasChat,
|
hasChat,
|
||||||
|
@ -48,8 +51,7 @@ export const useChatIndividualContext = ({
|
||||||
selectedFileType,
|
selectedFileType,
|
||||||
chatMessageIds,
|
chatMessageIds,
|
||||||
chatId,
|
chatId,
|
||||||
isNewChat,
|
isLoading
|
||||||
isFollowUpChat
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,20 @@
|
||||||
import { IBusterChat, useBusterChatContextSelector } from '@/context/Chats';
|
import { useBusterChatContextSelector } from '@/context/Chats';
|
||||||
import { SelectedFile } from '../interfaces';
|
import type { SelectedFile } from '../interfaces';
|
||||||
import { usePrevious } from 'ahooks';
|
import { usePrevious } from 'ahooks';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const useAutoChangeLayout = ({
|
export const useAutoChangeLayout = ({
|
||||||
lastMessageId,
|
lastMessageId,
|
||||||
chat,
|
|
||||||
onSetSelectedFile
|
onSetSelectedFile
|
||||||
}: {
|
}: {
|
||||||
lastMessageId: string;
|
lastMessageId: string;
|
||||||
chat: IBusterChat | undefined;
|
|
||||||
onSetSelectedFile: (file: SelectedFile) => void;
|
onSetSelectedFile: (file: SelectedFile) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const message = useBusterChatContextSelector((x) => x.chatsMessages[lastMessageId]);
|
const message = useBusterChatContextSelector((x) => x.chatsMessages[lastMessageId]);
|
||||||
const reasoningMessagesLength = message?.reasoning?.length;
|
const reasoningMessagesLength = message?.reasoning?.length;
|
||||||
const previousReasoningMessagesLength = usePrevious(reasoningMessagesLength);
|
const previousReasoningMessagesLength = usePrevious(reasoningMessagesLength);
|
||||||
const isCompletedStream = message?.isCompletedStream;
|
const isCompletedStream = message?.isCompletedStream;
|
||||||
const isFollowupMessage = chat?.isFollowupMessage;
|
const isLoading = !isCompletedStream;
|
||||||
const isNewChat = chat?.isNewChat;
|
|
||||||
const isLoading = isNewChat || isFollowupMessage || !isCompletedStream;
|
|
||||||
const hasReasoning = !!reasoningMessagesLength;
|
const hasReasoning = !!reasoningMessagesLength;
|
||||||
const previousIsEmpty = previousReasoningMessagesLength === 0;
|
const previousIsEmpty = previousReasoningMessagesLength === 0;
|
||||||
|
|
||||||
|
|
|
@ -74,8 +74,6 @@ export const useFileFallback = ({
|
||||||
const fallbackToFileChat = ({ id }: { id: string }): IBusterChat => {
|
const fallbackToFileChat = ({ id }: { id: string }): IBusterChat => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
isNewChat: false,
|
|
||||||
isFollowupMessage: false,
|
|
||||||
messages: [fallbackMessageId(id)],
|
messages: [fallbackMessageId(id)],
|
||||||
title: '',
|
title: '',
|
||||||
is_favorited: false,
|
is_favorited: false,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
useContextSelector
|
useContextSelector
|
||||||
} from '@fluentui/react-context-selector';
|
} from '@fluentui/react-context-selector';
|
||||||
import type { BusterChat } from '@/api/asset_interfaces';
|
import type { BusterChat } from '@/api/asset_interfaces';
|
||||||
import { IBusterChat, IBusterChatMessage } from '../interfaces';
|
import type { IBusterChat, IBusterChatMessage } from '../interfaces';
|
||||||
import { useChatSubscriptions } from './useChatSubscriptions';
|
import { useChatSubscriptions } from './useChatSubscriptions';
|
||||||
import { useChatAssosciations } from './useChatAssosciations';
|
import { useChatAssosciations } from './useChatAssosciations';
|
||||||
import { useChatSelectors } from './useChatSelectors';
|
import { useChatSelectors } from './useChatSelectors';
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './chatUpgrader';
|
export * from './useFileFallback';
|
||||||
|
|
|
@ -61,8 +61,6 @@ export const useFileFallback = ({
|
||||||
const fallbackToFileChat = ({ id }: { id: string }): IBusterChat => {
|
const fallbackToFileChat = ({ id }: { id: string }): IBusterChat => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
isNewChat: true,
|
|
||||||
isFollowupMessage: false,
|
|
||||||
messages: [fallbackMessageId(id)],
|
messages: [fallbackMessageId(id)],
|
||||||
title: '',
|
title: '',
|
||||||
is_favorited: false,
|
is_favorited: false,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { MutableRefObject, useCallback } from 'react';
|
import { type MutableRefObject, useCallback } from 'react';
|
||||||
import { IBusterChat, IBusterChatMessage } from '../interfaces';
|
import type { IBusterChat, IBusterChatMessage } from '../interfaces';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
|
||||||
export const useChatSelectors = ({
|
export const useChatSelectors = ({
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { MutableRefObject } from 'react';
|
||||||
import { useBusterWebSocket } from '../../BusterWebSocket';
|
import { useBusterWebSocket } from '../../BusterWebSocket';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import type { BusterChat } from '@/api/asset_interfaces';
|
import type { BusterChat } from '@/api/asset_interfaces';
|
||||||
import { IBusterChat, IBusterChatMessage } from '../interfaces';
|
import type { IBusterChat, IBusterChatMessage } from '../interfaces';
|
||||||
import { chatMessageUpgrader, chatUpgrader } from './helpers';
|
import { updateChatToIChat } from '@/utils/chat';
|
||||||
import { MOCK_CHAT } from './MOCK_CHAT';
|
import { MOCK_CHAT } from './MOCK_CHAT';
|
||||||
|
|
||||||
export const useChatSubscriptions = ({
|
export const useChatSubscriptions = ({
|
||||||
|
@ -18,17 +18,17 @@ export const useChatSubscriptions = ({
|
||||||
const busterSocket = useBusterWebSocket();
|
const busterSocket = useBusterWebSocket();
|
||||||
|
|
||||||
const _onGetChat = useMemoizedFn((chat: BusterChat): IBusterChat => {
|
const _onGetChat = useMemoizedFn((chat: BusterChat): IBusterChat => {
|
||||||
const upgradedChat = chatUpgrader(chat);
|
const { iChat, iChatMessages } = updateChatToIChat(chat);
|
||||||
const upgradedChatMessages = chatMessageUpgrader(chat.messages);
|
|
||||||
chatsRef.current[chat.id] = upgradedChat;
|
chatsRef.current[chat.id] = iChat;
|
||||||
chatsMessagesRef.current = {
|
chatsMessagesRef.current = {
|
||||||
...chatsMessagesRef.current,
|
...chatsMessagesRef.current,
|
||||||
...upgradedChatMessages
|
...iChatMessages
|
||||||
};
|
};
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
//just used to trigger UI update
|
//just used to trigger UI update
|
||||||
});
|
});
|
||||||
return upgradedChat;
|
return iChat;
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribeFromChat = useMemoizedFn(({ chatId }: { chatId: string }) => {
|
const unsubscribeFromChat = useMemoizedFn(({ chatId }: { chatId: string }) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useBusterWebSocket } from '@/context/BusterWebSocket';
|
import { useBusterWebSocket } from '@/context/BusterWebSocket';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { MutableRefObject } from 'react';
|
import { type MutableRefObject } from 'react';
|
||||||
import { IBusterChat, IBusterChatMessage } from '../interfaces';
|
import type { IBusterChat, IBusterChatMessage } from '../interfaces';
|
||||||
|
|
||||||
export const useChatUpdate = ({
|
export const useChatUpdate = ({
|
||||||
chatsRef,
|
chatsRef,
|
||||||
|
@ -15,13 +15,25 @@ export const useChatUpdate = ({
|
||||||
const busterSocket = useBusterWebSocket();
|
const busterSocket = useBusterWebSocket();
|
||||||
|
|
||||||
const onUpdateChat = useMemoizedFn(
|
const onUpdateChat = useMemoizedFn(
|
||||||
async (newChatConfig: Partial<IBusterChat> & { id: string }) => {
|
async (newChatConfig: Partial<IBusterChat> & { id: string }, saveToServer: boolean = false) => {
|
||||||
chatsRef.current[newChatConfig.id] = {
|
chatsRef.current[newChatConfig.id] = {
|
||||||
...chatsRef.current[newChatConfig.id],
|
...chatsRef.current[newChatConfig.id],
|
||||||
...newChatConfig
|
...newChatConfig
|
||||||
};
|
};
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
//just used to trigger UI update
|
//just used to trigger UI update
|
||||||
|
|
||||||
|
if (saveToServer) {
|
||||||
|
const { title, is_favorited, id } = chatsRef.current[newChatConfig.id];
|
||||||
|
busterSocket.emit({
|
||||||
|
route: '/chats/update',
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
is_favorited
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -38,8 +50,21 @@ export const useChatUpdate = ({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onBulkSetChatMessages = useMemoizedFn(
|
||||||
|
(newMessagesConfig: Record<string, IBusterChatMessage>) => {
|
||||||
|
chatsMessagesRef.current = {
|
||||||
|
...chatsMessagesRef.current,
|
||||||
|
...newMessagesConfig
|
||||||
|
};
|
||||||
|
startTransition(() => {
|
||||||
|
//just used to trigger UI update
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onUpdateChat,
|
onUpdateChat,
|
||||||
onUpdateChatMessage
|
onUpdateChatMessage,
|
||||||
|
onBulkSetChatMessages
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
useContextSelector
|
useContextSelector
|
||||||
} from '@fluentui/react-context-selector';
|
} from '@fluentui/react-context-selector';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import type { BusterDatasetListItem, BusterSearchResult, FileType } from '@/api/asset_interfaces';
|
import type { BusterSearchResult, FileType } from '@/api/asset_interfaces';
|
||||||
import { useBusterWebSocket } from '@/context/BusterWebSocket';
|
import { useBusterWebSocket } from '@/context/BusterWebSocket';
|
||||||
import { useChatUpdateMessage } from './useChatUpdateMessage';
|
import { useChatUpdateMessage } from './useChatUpdateMessage';
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ChatEvent_GeneratingResponseMessage,
|
ChatEvent_GeneratingResponseMessage,
|
||||||
ChatEvent_GeneratingTitle
|
ChatEvent_GeneratingTitle
|
||||||
} from '@/api/buster_socket/chats';
|
} from '@/api/buster_socket/chats';
|
||||||
|
import { updateChatToIChat } from '@/utils/chat';
|
||||||
|
|
||||||
export const useChatUpdateMessage = () => {
|
export const useChatUpdateMessage = () => {
|
||||||
const busterSocket = useBusterWebSocket();
|
const busterSocket = useBusterWebSocket();
|
||||||
|
@ -14,6 +15,7 @@ export const useChatUpdateMessage = () => {
|
||||||
const getChatMemoized = useBusterChatContextSelector((x) => x.getChatMemoized);
|
const getChatMemoized = useBusterChatContextSelector((x) => x.getChatMemoized);
|
||||||
const onUpdateChatMessage = useBusterChatContextSelector((x) => x.onUpdateChatMessage);
|
const onUpdateChatMessage = useBusterChatContextSelector((x) => x.onUpdateChatMessage);
|
||||||
const getChatMessageMemoized = useBusterChatContextSelector((x) => x.getChatMessageMemoized);
|
const getChatMessageMemoized = useBusterChatContextSelector((x) => x.getChatMessageMemoized);
|
||||||
|
const onBulkSetChatMessages = useBusterChatContextSelector((x) => x.onBulkSetChatMessages);
|
||||||
|
|
||||||
const _generatingTitleCallback = useMemoizedFn((d: ChatEvent_GeneratingTitle) => {
|
const _generatingTitleCallback = useMemoizedFn((d: ChatEvent_GeneratingTitle) => {
|
||||||
const { chat_id, title, title_chunk } = d;
|
const { chat_id, title, title_chunk } = d;
|
||||||
|
@ -57,10 +59,9 @@ export const useChatUpdateMessage = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const completeChatCallback = useMemoizedFn((d: BusterChat) => {
|
const completeChatCallback = useMemoizedFn((d: BusterChat) => {
|
||||||
onUpdateChatMessage({
|
const { iChat, iChatMessages } = updateChatToIChat(d);
|
||||||
...d,
|
onBulkSetChatMessages(iChatMessages);
|
||||||
isCompletedStream: true
|
onUpdateChat(iChat);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const stopChatCallback = useMemoizedFn((chatId: string) => {
|
const stopChatCallback = useMemoizedFn((chatId: string) => {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import type { BusterChat, BusterChatMessage } from '@/api/asset_interfaces';
|
import type { BusterChat, BusterChatMessage } from '@/api/asset_interfaces';
|
||||||
|
|
||||||
export interface IBusterChat extends Omit<BusterChat, 'messages'> {
|
export interface IBusterChat extends Omit<BusterChat, 'messages'> {
|
||||||
isNewChat: boolean;
|
|
||||||
isFollowupMessage: boolean;
|
|
||||||
messages: string[];
|
messages: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
import type { BusterChat, BusterChatMessage } from '@/api/asset_interfaces';
|
import type { BusterChat, BusterChatMessage } from '@/api/asset_interfaces';
|
||||||
import type { IBusterChat, IBusterChatMessage } from '../../interfaces';
|
import type { IBusterChat, IBusterChatMessage } from '@/context/Chats/interfaces';
|
||||||
|
|
||||||
export const chatUpgrader = (
|
const chatUpgrader = (chat: BusterChat): IBusterChat => {
|
||||||
chat: BusterChat,
|
|
||||||
options?: { isNewChat?: boolean; isFollowupMessage?: boolean }
|
|
||||||
): IBusterChat => {
|
|
||||||
const { isNewChat = false, isFollowupMessage = false } = options || {};
|
|
||||||
return {
|
return {
|
||||||
...chat,
|
...chat,
|
||||||
isNewChat,
|
|
||||||
isFollowupMessage,
|
|
||||||
messages: chat.messages.map((message) => message.id)
|
messages: chat.messages.map((message) => message.id)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const chatMessageUpgrader = (
|
const chatMessageUpgrader = (
|
||||||
message: BusterChatMessage[],
|
message: BusterChatMessage[],
|
||||||
options?: { isCompletedStream: boolean; messageId: string }
|
options?: { isCompletedStream: boolean; messageId: string }
|
||||||
): Record<string, IBusterChatMessage> => {
|
): Record<string, IBusterChatMessage> => {
|
||||||
|
@ -40,3 +34,12 @@ export const chatMessageUpgrader = (
|
||||||
{} as Record<string, IBusterChatMessage>
|
{} as Record<string, IBusterChatMessage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateChatToIChat = (chat: BusterChat) => {
|
||||||
|
const iChat = chatUpgrader(chat);
|
||||||
|
const iChatMessages = chatMessageUpgrader(chat.messages);
|
||||||
|
return {
|
||||||
|
iChat,
|
||||||
|
iChatMessages
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue