mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +00:00
perf: chat ui
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
接受一个 csv 文件,表格头包含 question 和 answer。question 代表问题,answer 代表答案。
|
||||
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
|
||||
**请保证 csv 文件为 utf-8 编码**
|
||||
| question | answer |
|
||||
| --- | --- |
|
||||
| 什么是 laf | laf 是一个云函数开发平台…… |
|
||||
|
@@ -27,7 +27,7 @@ export const postSaveChat = (data: {
|
||||
modelId: string;
|
||||
newChatId: '' | string;
|
||||
chatId: '' | string;
|
||||
prompts: ChatItemType[];
|
||||
prompts: [ChatItemType, ChatItemType];
|
||||
}) => POST<string>('/chat/saveChat', data);
|
||||
|
||||
/**
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
|
@@ -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} />
|
||||
退出聊天
|
||||
|
@@ -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,20 +645,27 @@ 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')}
|
||||
>
|
||||
{!isPc && (
|
||||
<MyIcon
|
||||
name={'tabbarMore'}
|
||||
w={'14px'}
|
||||
@@ -616,8 +673,19 @@ const Chat = ({
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
onClick={onOpenSlider}
|
||||
/>
|
||||
<Box>{chatData.model.name}</Box>
|
||||
{chatId && (
|
||||
)}
|
||||
<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(
|
||||
<RenderContextMenu
|
||||
history={messageContextMenuData.message}
|
||||
index={chatData.history.findIndex(
|
||||
(item) => item._id === messageContextMenuData.message._id
|
||||
),
|
||||
messageContextMenuData.message._id
|
||||
)
|
||||
}
|
||||
>
|
||||
删除
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
)}
|
||||
AiDetail={!isPc}
|
||||
/>
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
|
@@ -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'}
|
||||
|
@@ -17,7 +17,7 @@ const modelList = () => {
|
||||
/* 加载模型 */
|
||||
const { data, isLoading, Pagination, getData, pageNum } = usePagination<ShareModelItem>({
|
||||
api: getShareModelList,
|
||||
pageSize: 20,
|
||||
pageSize: 24,
|
||||
params: {
|
||||
searchText
|
||||
}
|
||||
|
@@ -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()
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user