perf: quote modal

This commit is contained in:
archer
2023-07-23 22:24:14 +08:00
parent 1ffe1be562
commit ea35ad2144
10 changed files with 188 additions and 209 deletions

View File

@@ -13,21 +13,17 @@ import MyIcon from '@/components/Icon';
import InputDataModal from '@/pages/kb/detail/components/InputDataModal'; import InputDataModal from '@/pages/kb/detail/components/InputDataModal';
import { getKbDataItemById } from '@/api/plugins/kb'; import { getKbDataItemById } from '@/api/plugins/kb';
import { useLoading } from '@/hooks/useLoading'; import { useLoading } from '@/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';
import { getHistoryQuote, updateHistoryQuote } from '@/api/chat';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { getErrText } from '@/utils/tools'; import { getErrText } from '@/utils/tools';
import { QuoteItemType } from '@/types/chat'; import { QuoteItemType } from '@/types/chat';
const QuoteModal = ({ const QuoteModal = ({
chatId, onUpdateQuote,
contentId,
rawSearch = [], rawSearch = [],
onClose onClose
}: { }: {
chatId?: string; onUpdateQuote: (quoteId: string, sourceText: string) => Promise<void>;
contentId?: string; rawSearch: QuoteItemType[];
rawSearch?: QuoteItemType[];
onClose: () => void; onClose: () => void;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@@ -40,47 +36,6 @@ const QuoteModal = ({
q: string; q: string;
}>(); }>();
const {
data: quote = [],
refetch,
isLoading
} = useQuery(['getHistoryQuote'], () => {
if (chatId && contentId) {
return getHistoryQuote({ chatId, contentId });
}
if (rawSearch.length > 0) {
return rawSearch;
}
return [];
});
/**
* update kbData, update mongo status and reload quotes
*/
const updateQuoteStatus = useCallback(
async (quoteId: string, sourceText: string) => {
if (!chatId || !contentId) return;
setIsLoading(true);
try {
await updateHistoryQuote({
contentId,
chatId,
quoteId,
sourceText
});
// reload quote
refetch();
} catch (err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
setIsLoading(false);
},
[contentId, chatId, refetch, setIsLoading, toast]
);
/** /**
* click edit, get new kbDataItem * click edit, get new kbDataItem
*/ */
@@ -91,7 +46,7 @@ const QuoteModal = ({
const data = (await getKbDataItemById(item.id)) as QuoteItemType; const data = (await getKbDataItemById(item.id)) as QuoteItemType;
if (!data) { if (!data) {
updateQuoteStatus(item.id, '已删除'); onUpdateQuote(item.id, '已删除');
throw new Error('该数据已被删除'); throw new Error('该数据已被删除');
} }
@@ -109,7 +64,7 @@ const QuoteModal = ({
} }
setIsLoading(false); setIsLoading(false);
}, },
[setIsLoading, toast, updateQuoteStatus] [setIsLoading, toast, onUpdateQuote]
); );
return ( return (
@@ -123,14 +78,14 @@ const QuoteModal = ({
overflow={'overlay'} overflow={'overlay'}
> >
<ModalHeader> <ModalHeader>
({quote.length}) ({rawSearch.length})
<Box fontSize={'sm'} fontWeight={'normal'}> <Box fontSize={'sm'} fontWeight={'normal'}>
注意: 修改知识库内容成功后 注意: 修改知识库内容成功后
</Box> </Box>
</ModalHeader> </ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody pt={0} whiteSpace={'pre-wrap'} textAlign={'justify'} fontSize={'sm'}> <ModalBody pt={0} whiteSpace={'pre-wrap'} textAlign={'justify'} fontSize={'sm'}>
{quote.map((item) => ( {rawSearch.map((item) => (
<Box <Box
key={item.id} key={item.id}
flex={'1 0 0'} flex={'1 0 0'}
@@ -172,14 +127,14 @@ const QuoteModal = ({
</Box> </Box>
))} ))}
</ModalBody> </ModalBody>
<Loading loading={isLoading} fixed={false} /> <Loading fixed={false} />
</ModalContent> </ModalContent>
</Modal> </Modal>
{editDataItem && ( {editDataItem && (
<InputDataModal <InputDataModal
onClose={() => setEditDataItem(undefined)} onClose={() => setEditDataItem(undefined)}
onSuccess={() => updateQuoteStatus(editDataItem.dataId, '手动修改')} onSuccess={() => onUpdateQuote(editDataItem.dataId, '手动修改')}
onDelete={() => updateQuoteStatus(editDataItem.dataId, '已删除')} onDelete={() => onUpdateQuote(editDataItem.dataId, '已删除')}
kbId={editDataItem.kbId} kbId={editDataItem.kbId}
defaultValues={editDataItem} defaultValues={editDataItem}
/> />

View File

@@ -1,7 +1,96 @@
import React from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { ChatModuleEnum } from '@/constants/chat';
import { ChatHistoryItemResType, QuoteItemType } from '@/types/chat';
import { Flex, BoxProps } from '@chakra-ui/react';
import { updateHistoryQuote } from '@/api/chat';
import dynamic from 'next/dynamic';
import Tag from '../Tag';
import MyTooltip from '../MyTooltip';
const QuoteModal = dynamic(() => import('./QuoteModal'), { ssr: false });
const ResponseDetailModal = () => { const ResponseDetailModal = ({
return <div>ResponseDetailModal</div>; chatId,
contentId,
responseData = []
}: {
chatId?: string;
contentId?: string;
responseData?: ChatHistoryItemResType[];
}) => {
console.log(responseData);
const [quoteModalData, setQuoteModalData] = useState<QuoteItemType[]>();
const {
tokens = 0,
quoteList = [],
completeMessages = []
} = useMemo(() => {
const chatData = responseData.find((item) => item.moduleName === ChatModuleEnum.AIChat);
if (!chatData) return {};
return {
tokens: chatData.tokens,
quoteList: chatData.quoteList,
completeMessages: chatData.completeMessages
};
}, [responseData]);
const isEmpty = useMemo(
() => quoteList.length === 0 && completeMessages.length === 0 && tokens === 0,
[completeMessages.length, quoteList.length, tokens]
);
const updateQuote = useCallback(async (quoteId: string, sourceText: string) => {}, []);
const TagStyles: BoxProps = {
mr: 2,
bg: 'transparent'
};
return isEmpty ? null : (
<Flex alignItems={'center'} mt={2} flexWrap={'wrap'}>
{quoteList.length > 0 && (
<MyTooltip label="查看引用">
<Tag
colorSchema="blue"
cursor={'pointer'}
{...TagStyles}
onClick={() => setQuoteModalData(quoteList)}
>
{quoteList.length}
</Tag>
</MyTooltip>
)}
{completeMessages.length > 0 && (
<Tag colorSchema="green" cursor={'default'} {...TagStyles}>
{completeMessages.length}
</Tag>
)}
{tokens > 0 && (
<Tag colorSchema="gray" cursor={'default'} {...TagStyles}>
{tokens}tokens
</Tag>
)}
{/* <Button
size={'sm'}
variant={'base'}
borderRadius={'md'}
fontSize={'xs'}
px={2}
lineHeight={1}
py={1}
>
完整参数
</Button> */}
{!!quoteModalData && (
<QuoteModal
rawSearch={quoteModalData}
onUpdateQuote={updateQuote}
onClose={() => setQuoteModalData(undefined)}
/>
)}
</Flex>
);
}; };
export default ResponseDetailModal; export default ResponseDetailModal;

View File

@@ -23,7 +23,7 @@ import {
hasVoiceApi, hasVoiceApi,
getErrText getErrText
} from '@/utils/tools'; } from '@/utils/tools';
import { Box, Card, Flex, Input, Textarea, Button, useTheme } from '@chakra-ui/react'; import { Box, Card, Flex, Input, Textarea, Button, useTheme, BoxProps } from '@chakra-ui/react';
import { feConfigs } from '@/store/static'; import { feConfigs } from '@/store/static';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { EventNameEnum } from '../Markdown/constant'; import { EventNameEnum } from '../Markdown/constant';
@@ -38,12 +38,11 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common'; import { htmlTemplate } from '@/constants/common';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { QuoteItemType } from '@/types/chat';
import { FlowModuleTypeEnum } from '@/constants/flow'; import { FlowModuleTypeEnum } from '@/constants/flow';
import { TaskResponseKeyEnum } from '@/constants/chat'; import { TaskResponseKeyEnum } from '@/constants/chat';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
const QuoteModal = dynamic(() => import('./QuoteModal')); const ResponseDetailModal = dynamic(() => import('./ResponseDetailModal'));
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
@@ -108,15 +107,22 @@ const Empty = () => {
); );
}; };
const ChatAvatar = ({ const ChatAvatar = ({ src, type }: { src?: string; type: 'Human' | 'AI' }) => {
src, const theme = useTheme();
ml, return (
mr <Box
}: { w={['28px', '34px']}
src?: string; h={['28px', '34px']}
ml?: (string | number) | (string | number)[]; p={'2px'}
mr?: (string | number) | (string | number)[]; borderRadius={'lg'}
}) => <Avatar src={src} w={['24px', '34px']} h={['24px', '34px']} ml={ml} mr={mr} />; border={theme.borders.base}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
bg={type === 'Human' ? 'white' : 'myBlue.100'}
>
<Avatar src={src} w={'100%'} h={'100%'} />
</Box>
);
};
const ChatBox = ( const ChatBox = (
{ {
@@ -157,10 +163,6 @@ const ChatBox = (
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [variables, setVariables] = useState<Record<string, any>>({}); const [variables, setVariables] = useState<Record<string, any>>({});
const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]); const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]);
const [quoteModalData, setQuoteModalData] = useState<{
contentId?: string;
rawSearch?: QuoteItemType[];
}>();
const isChatting = useMemo( const isChatting = useMemo(
() => chatHistory[chatHistory.length - 1]?.status === 'loading', () => chatHistory[chatHistory.length - 1]?.status === 'loading',
@@ -394,13 +396,16 @@ const ChatBox = (
color: 'myGray.400', color: 'myGray.400',
display: ['flex', 'none'], display: ['flex', 'none'],
pl: 1, pl: 1,
mt: 2, mt: 2
position: 'absolute' as any, };
zIndex: 1, const MessageCardStyle: BoxProps = {
w: '100%' px: 4,
py: 3,
borderRadius: '0 8px 8px 8px',
boxShadow: '0 0 8px rgba(0,0,0,0.15)'
}; };
const messageCardMaxW = ['calc(100% - 48px)', 'calc(100% - 65px)']; const messageCardMaxW = ['calc(100% - 25px)', 'calc(100% - 40px)'];
const showEmpty = useMemo( const showEmpty = useMemo(
() => () =>
@@ -439,27 +444,19 @@ const ChatBox = (
h={0} h={0}
w={'100%'} w={'100%'}
overflow={'overlay'} overflow={'overlay'}
px={[2, 5]} px={[4, 0]}
mt={[0, 5]} mt={[0, 5]}
pb={3} pb={3}
> >
<Box maxW={['100%', '1000px', '1200px']} h={'100%'} mx={'auto'}> <Box maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
{showEmpty && <Empty />} {showEmpty && <Empty />}
{!!welcomeText && ( {!!welcomeText && (
<Flex alignItems={'flex-start'} py={2}> <Flex flexDirection={'column'} alignItems={'flex-start'} py={2}>
{/* avatar */} {/* avatar */}
<ChatAvatar src={appAvatar} mr={['6px', 2]} /> <ChatAvatar src={appAvatar} type={'AI'} />
{/* message */} {/* message */}
<Card <Card order={2} mt={2} {...MessageCardStyle} bg={'white'} maxW={messageCardMaxW}>
order={2}
mt={2}
px={4}
py={3}
bg={'white'}
maxW={messageCardMaxW}
borderRadius={'0 8px 8px 8px'}
>
<Markdown <Markdown
source={`~~~guide \n${welcomeText}`} source={`~~~guide \n${welcomeText}`}
isChatting={false} isChatting={false}
@@ -474,19 +471,17 @@ const ChatBox = (
)} )}
{/* variable input */} {/* variable input */}
{!!variableModules?.length && ( {!!variableModules?.length && (
<Flex alignItems={'flex-start'} py={2}> <Flex flexDirection={'column'} alignItems={'flex-start'} py={2}>
{/* avatar */} {/* avatar */}
<ChatAvatar src={appAvatar} mr={['6px', 2]} /> <ChatAvatar src={appAvatar} type={'AI'} />
{/* message */} {/* message */}
<Card <Card
order={2} order={2}
mt={2} mt={2}
bg={'white'} bg={'white'}
px={4} w={'400px'}
py={3}
borderRadius={'0 8px 8px 8px'}
flex={'0 0 400px'}
maxW={messageCardMaxW} maxW={messageCardMaxW}
{...MessageCardStyle}
> >
{variableModules.map((item) => ( {variableModules.map((item) => (
<Box key={item.id} mb={4}> <Box key={item.id} mb={4}>
@@ -540,8 +535,8 @@ const ChatBox = (
<Flex <Flex
position={'relative'} position={'relative'}
key={item._id} key={item._id}
alignItems={'flex-start'} flexDirection={'column'}
justifyContent={item.obj === 'Human' ? 'flex-end' : 'flex-start'} alignItems={item.obj === 'Human' ? 'flex-end' : 'flex-start'}
py={5} py={5}
_hover={{ _hover={{
'& .control': { '& .control': {
@@ -551,18 +546,8 @@ const ChatBox = (
> >
{item.obj === 'Human' && ( {item.obj === 'Human' && (
<> <>
<Box position={'relative'} maxW={messageCardMaxW}> <Flex w={'100%'} alignItems={'center'} justifyContent={'flex-end'}>
<Card <Flex {...controlContainerStyle} justifyContent={'flex-end'} mr={3}>
className="markdown"
whiteSpace={'pre-wrap'}
px={4}
py={3}
borderRadius={'8px 0 8px 8px'}
bg={'myBlue.300'}
>
<Box as={'p'}>{item.value}</Box>
</Card>
<Flex {...controlContainerStyle} justifyContent={'flex-end'}>
<MyTooltip label={'复制'}> <MyTooltip label={'复制'}>
<MyIcon <MyIcon
{...controlIconStyle} {...controlIconStyle}
@@ -591,38 +576,25 @@ const ChatBox = (
</MyTooltip> </MyTooltip>
)} )}
</Flex> </Flex>
<ChatAvatar src={userAvatar} type={'Human'} />
</Flex>
<Box position={'relative'} maxW={messageCardMaxW} mt={['6px', 2]}>
<Card
className="markdown"
whiteSpace={'pre-wrap'}
{...MessageCardStyle}
bg={'myBlue.300'}
>
<Box as={'p'}>{item.value}</Box>
</Card>
</Box> </Box>
<ChatAvatar src={userAvatar} ml={['6px', 2]} />
</> </>
)} )}
{item.obj === 'AI' && ( {item.obj === 'AI' && (
<> <>
<ChatAvatar src={appAvatar} mr={['6px', 2]} /> <Flex w={'100%'} alignItems={'center'}>
<Box position={'relative'} maxW={messageCardMaxW}> <ChatAvatar src={appAvatar} type={'AI'} />
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}> <Flex {...controlContainerStyle} ml={3}>
<Markdown
source={item.value}
isChatting={index === chatHistory.length - 1 && isChatting}
/>
{/* {(!!item[quoteLenKey] || !!item[rawSearchKey]?.length) && (
<Button
size={'xs'}
variant={'base'}
mt={2}
w={'80px'}
onClick={() => {
setQuoteModalData({
contentId: item._id,
rawSearch: item[rawSearchKey]
});
}}
>
{item[quoteLenKey] || item[rawSearchKey]?.length}条引用
</Button>
)} */}
</Card>
<Flex {...controlContainerStyle}>
<MyTooltip label={'复制'}> <MyTooltip label={'复制'}>
<MyIcon <MyIcon
{...controlIconStyle} {...controlIconStyle}
@@ -660,6 +632,19 @@ const ChatBox = (
</MyTooltip> </MyTooltip>
)} )}
</Flex> </Flex>
</Flex>
<Box position={'relative'} maxW={messageCardMaxW} mt={['6px', 2]}>
<Card bg={'white'} {...MessageCardStyle}>
<Markdown
source={item.value}
isChatting={index === chatHistory.length - 1 && isChatting}
/>
<ResponseDetailModal
chatId={chatId}
contentId={item._id}
responseData={item.responseData}
/>
</Card>
</Box> </Box>
</> </>
)} )}
@@ -753,14 +738,6 @@ const ChatBox = (
</Box> </Box>
</Box> </Box>
) : null} ) : null}
{/* quote modal */}
{!!quoteModalData && (
<QuoteModal
chatId={chatId}
{...quoteModalData}
onClose={() => setQuoteModalData(undefined)}
/>
)}
</Flex> </Flex>
); );
}; };

View File

@@ -1,8 +1,14 @@
import React from 'react'; import React from 'react';
import { Tooltip, TooltipProps } from '@chakra-ui/react'; import { Tooltip, TooltipProps } from '@chakra-ui/react';
import { useGlobalStore } from '@/store/global';
const MyTooltip = ({ children, ...props }: TooltipProps) => { interface Props extends TooltipProps {
return ( forceShow?: boolean;
}
const MyTooltip = ({ children, forceShow = false, ...props }: Props) => {
const { isPc } = useGlobalStore();
return isPc || forceShow ? (
<Tooltip <Tooltip
bg={'white'} bg={'white'}
arrowShadowColor={' rgba(0,0,0,0.1)'} arrowShadowColor={' rgba(0,0,0,0.1)'}
@@ -19,6 +25,8 @@ const MyTooltip = ({ children, ...props }: TooltipProps) => {
> >
{children} {children}
</Tooltip> </Tooltip>
) : (
<>{children}</>
); );
}; };

View File

@@ -1,52 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, contentId } = req.query as {
chatId: string;
contentId: string;
};
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
if (!chatId || !contentId) {
throw new Error('params is error');
}
const history = await Chat.aggregate([
{
$match: {
_id: new Types.ObjectId(chatId),
userId: new Types.ObjectId(userId)
}
},
{
$unwind: '$content'
},
{
$match: {
'content._id': new Types.ObjectId(contentId)
}
},
{
$project: {
// [rawSearchKey]: `$content.${rawSearchKey}`
}
}
]);
jsonRes(res, {
// data: history[0]?.[rawSearchKey] || []
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat, App } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
import type { InitChatResponse } from '@/api/response/chat'; import type { InitChatResponse } from '@/api/response/chat';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
@@ -59,7 +59,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{ {
$project: { $project: {
content: { content: {
$slice: ['$content', -50] // 返回 content 数组的最后50个元素 $slice: ['$content', -30] // 返回 content 数组的最后 30 个元素
} }
} }
}, },

View File

@@ -36,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
export async function getChatHistory({ export async function getChatHistory({
chatId, chatId,
userId, userId,
limit = 50 limit = 20
}: Props & { userId: string }): Promise<Response> { }: Props & { userId: string }): Promise<Response> {
if (!chatId) { if (!chatId) {
return { history: [] }; return { history: [] };
@@ -47,7 +47,7 @@ export async function getChatHistory({
{ {
$project: { $project: {
content: { content: {
$slice: ['$content', -limit] // 返回 content 数组的最后50个元素 $slice: ['$content', -limit] // 返回 content 数组的最后20个元素
} }
} }
}, },

View File

@@ -140,7 +140,7 @@ const Settings = ({ appId }: { appId: string }) => {
}, [appModule2Form]); }, [appModule2Form]);
const BoxStyles: BoxProps = { const BoxStyles: BoxProps = {
bg: 'myWhite.300', bg: 'myWhite.200',
px: 4, px: 4,
py: 3, py: 3,
borderRadius: 'lg', borderRadius: 'lg',

View File

@@ -159,7 +159,7 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
if (res.history.length > 0) { if (res.history.length > 0) {
setTimeout(() => { setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto'); ChatBoxRef.current?.scrollToBottom('auto');
}, 200); }, 500);
} }
} catch (e: any) { } catch (e: any) {
// reset all chat tore // reset all chat tore

View File

@@ -131,7 +131,9 @@ const ShareChat = ({ shareId, chatId }: { shareId: string; chatId: string }) =>
} }
if (history.chats.length > 0) { if (history.chats.length > 0) {
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto'); ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
} }
return history; return history;