perf: chat ui

This commit is contained in:
archer
2023-05-12 20:41:42 +08:00
parent 3e5118c4f7
commit 4e0c876154
10 changed files with 162 additions and 107 deletions

View File

@@ -1,6 +1,7 @@
接受一个csv文件表格头包含 question 和 answer。question 代表问题answer 代表答案。
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
接受一个 csv 文件,表格头包含 question 和 answer。question 代表问题answer 代表答案。
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
**请保证 csv 文件为 utf-8 编码**
| question | answer |
| --- | --- |
| 什么是 laf | laf 是一个云函数开发平台…… |
| --- | --- |
| 什么是 laf | laf 是一个云函数开发平台…… |
| 什么是 sealos | Sealos 是以 kubernetes 为内核的云操作系统发行版,可以…… |

View File

@@ -27,7 +27,7 @@ export const postSaveChat = (data: {
modelId: string;
newChatId: '' | string;
chatId: '' | string;
prompts: ChatItemType[];
prompts: [ChatItemType, ChatItemType];
}) => POST<string>('/chat/saveChat', data);
/**

View File

@@ -16,12 +16,6 @@ const Navbar = () => {
const { lastChatModelId, lastChatId } = useChatStore();
const navbarList = useMemo(
() => [
{
label: 'AI助手',
icon: 'model',
link: `/model?modelId=${lastModelId}`,
activeLink: ['/model']
},
{
label: '聊天',
icon: 'chat',
@@ -29,6 +23,12 @@ const Navbar = () => {
activeLink: ['/chat']
},
{
label: 'AI助手',
icon: 'model',
link: `/model?modelId=${lastModelId}`,
activeLink: ['/model']
},
{
label: '共享',
icon: 'shareMarket',

View File

@@ -9,18 +9,18 @@ const NavbarPhone = () => {
const { lastChatModelId, lastChatId } = useChatStore();
const navbarList = useMemo(
() => [
{
label: 'AI助手',
icon: 'tabbarModel',
link: `/model`,
activeLink: ['/model']
},
{
label: '聊天',
icon: 'tabbarChat',
link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`,
activeLink: ['/chat']
},
{
label: 'AI助手',
icon: 'tabbarModel',
link: `/model`,
activeLink: ['/model']
},
{
label: '发现',
icon: 'tabbarMore',

View File

@@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
newChatId: '' | string;
chatId: '' | string;
modelId: string;
prompts: ChatItemType[];
prompts: [ChatItemType, ChatItemType];
};
if (!prompts) {
@@ -41,7 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelId,
content,
title: content[0].value.slice(0, 20),
latestChat: content[content.length - 1].value
latestChat: content[1].value
});
return jsonRes(res, {
data: _id
@@ -54,8 +54,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
$each: content
}
},
updateTime: new Date(),
latestChat: content[content.length - 1].value
title: content[0].value.slice(0, 20),
latestChat: content[1].value,
updateTime: new Date()
});
}
jsonRes(res);

View File

@@ -172,7 +172,7 @@ const PhoneSliderBar = ({
<Divider my={3} colorScheme={useColorModeValue('gray', 'white')} />
<RenderButton onClick={() => router.push('/')}>
<RenderButton onClick={() => router.push('/model')}>
<>
<MyIcon name="out" fill={'white'} w={'18px'} h={'18px'} mr={4} />
退

View File

@@ -29,19 +29,21 @@ import {
DrawerContent,
Card,
Tooltip,
useOutsideClick
useOutsideClick,
useTheme
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useCopyData } from '@/utils/tools';
import { useCopyData, voiceBroadcast } from '@/utils/tools';
import { streamFetch } from '@/api/fetch';
import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { Types } from 'mongoose';
import Markdown from '@/components/Markdown';
import { LOGO_ICON } from '@/constants/chat';
import { ChatModelMap } from '@/constants/model';
import { useChatStore } from '@/store/chat';
import { useLoading } from '@/hooks/useLoading';
import { fileDownload } from '@/utils/file';
@@ -75,7 +77,9 @@ const Chat = ({
chatId: string;
isPcDevice: boolean;
}) => {
const hasVoiceApi = !!window.speechSynthesis;
const router = useRouter();
const theme = useTheme();
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
@@ -118,6 +122,7 @@ const Chat = ({
const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
// close contextMenu
useOutsideClick({
ref: ContextMenuRef,
@@ -575,19 +580,64 @@ const Chat = ({
// abort stream
useEffect(() => {
return () => {
window.speechSynthesis?.cancel();
isLeavePage.current = true;
controller.current?.abort();
};
}, [modelId, chatId]);
// context menu component
const RenderContextMenu = useCallback(
({
history,
index,
AiDetail = false
}: {
history: ChatSiteItemType;
index: number;
AiDetail?: boolean;
}) => (
<MenuList fontSize={'sm'} minW={'100px !important'}>
{AiDetail && chatData.model.canUse && history.obj === 'AI' && (
<MenuItem
borderBottom={theme.borders.base}
onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}
>
AI助手详情
</MenuItem>
)}
<MenuItem onClick={() => onclickCopy(history.value)}></MenuItem>
{hasVoiceApi && (
<MenuItem
borderBottom={theme.borders.base}
onClick={() => voiceBroadcast({ text: history.value })}
>
</MenuItem>
)}
<MenuItem onClick={() => delChatRecord(index, history._id)}></MenuItem>
</MenuList>
),
[
chatData.model.canUse,
chatData.modelId,
delChatRecord,
hasVoiceApi,
onclickCopy,
router,
theme.borders.base
]
);
return (
<Flex
h={'100%'}
flexDirection={['column', 'row']}
backgroundColor={useColorModeValue('#fefefe', '')}
backgroundColor={useColorModeValue('#fdfdfd', '')}
>
{/* pc always show history. phone is only show when modelId is present */}
{isPc || !modelId ? (
{(isPc || !modelId) && (
<Box flex={[1, '0 0 250px']} w={['100%', 0]} h={'100%'}>
<History
onclickDelHistory={onclickDelHistory}
@@ -595,29 +645,47 @@ const Chat = ({
isPcDevice={isPcDevice}
/>
</Box>
) : (
<Box
h={'44px'}
borderBottom={'1px solid '}
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
)}
{/* 聊天内容 */}
{modelId && (
<Flex
position={'relative'}
h={[0, '100%']}
w={['100%', 0]}
flex={'1 0 0'}
flexDirection={'column'}
>
<Flex
alignItems={'center'}
h={'100%'}
justifyContent={'space-between'}
backgroundColor={useColorModeValue('white', 'gray.700')}
color={useColorModeValue('blackAlpha.700', 'white')}
py={[3, 5]}
px={5}
borderBottom={'1px solid '}
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
color={useColorModeValue('myGray.900', 'white')}
>
<MyIcon
name={'tabbarMore'}
w={'14px'}
h={'14px'}
color={useColorModeValue('blackAlpha.700', 'white')}
onClick={onOpenSlider}
/>
<Box>{chatData.model.name}</Box>
{chatId && (
{!isPc && (
<MyIcon
name={'tabbarMore'}
w={'14px'}
h={'14px'}
color={useColorModeValue('blackAlpha.700', 'white')}
onClick={onOpenSlider}
/>
)}
<Box
cursor={'pointer'}
lineHeight={1.2}
textAlign={'center'}
px={3}
fontSize={['sm', 'md']}
onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}
>
{chatData.model.name} {ChatModelMap[chatData.chatModel].name}
{chatData.history.length > 0 ? ` (${chatData.history.length})` : ''}
</Box>
{chatId ? (
<Menu autoSelect={false}>
<MenuButton lineHeight={1}>
<MyIcon
@@ -650,27 +718,10 @@ const Chat = ({
<MenuItem onClick={() => onclickExportChat('md')}>Markdown格式</MenuItem>
</MenuList>
</Menu>
) : (
<Box w={'16px'} h={'16px'} />
)}
</Flex>
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}>
<PhoneSliderBar chatId={chatId} modelId={modelId} onClose={onCloseSlider} />
</DrawerContent>
</Drawer>
</Box>
)}
{/* 聊天内容 */}
{modelId && (
<Flex
position={'relative'}
h={[0, '100%']}
w={['100%', 0]}
pt={[2, 4]}
flex={'1 0 0'}
flexDirection={'column'}
>
<Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'}>
<Box id={'history'} maxW={['auto', '800px', '1000px']} m={'auto'}>
{chatData.history.map((item, index) => (
@@ -683,13 +734,13 @@ const Chat = ({
{...(item.obj === 'AI'
? {
order: 1,
mr: ['6px', 4],
mr: ['6px', 2],
cursor: 'pointer',
onClick: () => isPc && router.push(`/model?modelId=${chatData.modelId}`)
}
: {
order: 3,
ml: ['6px', 4]
ml: ['6px', 2]
})}
>
<Tooltip label={item.obj === 'AI' ? 'AI助手详情' : ''}>
@@ -708,29 +759,12 @@ const Chat = ({
/>
</Tooltip>
</MenuButton>
{!isPc && (
<MenuList fontSize={'sm'} minW={'100px !important'}>
{chatData.model.canUse && item.obj === 'AI' && (
<MenuItem
onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}
>
AI助手详情
</MenuItem>
)}
<MenuItem onClick={() => onclickCopy(item.value)}></MenuItem>
<MenuItem onClick={() => delChatRecord(index, item._id)}></MenuItem>
</MenuList>
)}
{!isPc && <RenderContextMenu history={item} index={index} AiDetail />}
</Menu>
{/* message */}
<Flex order={2} maxW={['calc(100% - 50px)', '80%']}>
<Flex order={2} pt={2} maxW={['calc(100% - 50px)', '80%']}>
{item.obj === 'AI' ? (
<Box w={'100%'}>
{isPc && (
<Box color={'myGray.600'} fontSize={'sm'} mb={1}>
{chatData.model.name}
</Box>
)}
<Card
bg={'white'}
px={4}
@@ -759,11 +793,6 @@ const Chat = ({
</Box>
) : (
<Box>
{isPc && (
<Box color={'myGray.600'} mb={1} fontSize={'sm'} textAlign={'right'}>
Human
</Box>
)}
<Card
className="markdown"
whiteSpace={'pre-wrap'}
@@ -879,6 +908,15 @@ const Chat = ({
</Flex>
)}
{/* phone slider */}
{!isPc && (
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}>
<PhoneSliderBar chatId={chatId} modelId={modelId} onClose={onCloseSlider} />
</DrawerContent>
</Drawer>
)}
{/* system prompt show modal */}
{
<Modal isOpen={!!showSystemPrompt} onClose={() => setShowSystemPrompt('')}>
@@ -901,23 +939,13 @@ const Chat = ({
>
<Box ref={ContextMenuRef}></Box>
<Menu isOpen>
<MenuList minW={`100px !important`}>
<MenuItem onClick={() => onclickCopy(messageContextMenuData.message.value)}>
</MenuItem>
<MenuItem
onClick={() =>
delChatRecord(
chatData.history.findIndex(
(item) => item._id === messageContextMenuData.message._id
),
messageContextMenuData.message._id
)
}
>
</MenuItem>
</MenuList>
<RenderContextMenu
history={messageContextMenuData.message}
index={chatData.history.findIndex(
(item) => item._id === messageContextMenuData.message._id
)}
AiDetail={!isPc}
/>
</Menu>
</Box>
)}

View File

@@ -145,7 +145,7 @@ const ModelEditForm = ({
</Flex>
{isOwner && (
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}>AI和知识库</Box>
<Box flex={'0 0 120px'}>AI和知识库</Box>
<Button
colorScheme={'gray'}
variant={'outline'}

View File

@@ -17,7 +17,7 @@ const modelList = () => {
/* 加载模型 */
const { data, isLoading, Pagination, getData, pageNum } = usePagination<ShareModelItem>({
api: getShareModelList,
pageSize: 20,
pageSize: 24,
params: {
searchText
}

View File

@@ -88,3 +88,28 @@ export const formatTimeToChatTime = (time: Date) => {
// 如果是更久之前,展示某年某月某日
return target.format('YYYY/M/D');
};
/**
* voice broadcast
*/
export const voiceBroadcast = ({ text }: { text: string }) => {
window.speechSynthesis?.cancel();
const msg = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis?.getVoices?.(); // 获取语言包
const voice = voices.find((item) => {
return item.name === 'Microsoft Yaoyao - Chinese (Simplified, PRC)';
});
if (voice) {
msg.voice = voice;
}
window.speechSynthesis?.speak(msg);
msg.onerror = (e) => {
console.log(e);
};
return {
cancel: () => window.speechSynthesis?.cancel()
};
};