From 4e0c87615476c38d84ecdb043decfbdda8ebf576 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Fri, 12 May 2023 20:41:42 +0800 Subject: [PATCH] perf: chat ui --- public/docs/csvSelect.md | 9 +- src/api/chat.ts | 2 +- src/components/Layout/navbar.tsx | 12 +- src/components/Layout/navbarPhone.tsx | 12 +- src/pages/api/chat/saveChat.ts | 9 +- src/pages/chat/components/PhoneSliderBar.tsx | 2 +- src/pages/chat/index.tsx | 194 ++++++++++-------- .../detail/components/ModelEditForm.tsx | 2 +- src/pages/model/share/index.tsx | 2 +- src/utils/tools.ts | 25 +++ 10 files changed, 162 insertions(+), 107 deletions(-) diff --git a/public/docs/csvSelect.md b/public/docs/csvSelect.md index c5a2325b2..1246cd699 100644 --- a/public/docs/csvSelect.md +++ b/public/docs/csvSelect.md @@ -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 为内核的云操作系统发行版,可以…… | diff --git a/src/api/chat.ts b/src/api/chat.ts index e7eb088db..25bb230a4 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -27,7 +27,7 @@ export const postSaveChat = (data: { modelId: string; newChatId: '' | string; chatId: '' | string; - prompts: ChatItemType[]; + prompts: [ChatItemType, ChatItemType]; }) => POST('/chat/saveChat', data); /** diff --git a/src/components/Layout/navbar.tsx b/src/components/Layout/navbar.tsx index 56c826001..6df77975c 100644 --- a/src/components/Layout/navbar.tsx +++ b/src/components/Layout/navbar.tsx @@ -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', diff --git a/src/components/Layout/navbarPhone.tsx b/src/components/Layout/navbarPhone.tsx index 028eac59c..145d0a06b 100644 --- a/src/components/Layout/navbarPhone.tsx +++ b/src/components/Layout/navbarPhone.tsx @@ -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', diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index 4f41f8e76..75657c0f4 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -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); diff --git a/src/pages/chat/components/PhoneSliderBar.tsx b/src/pages/chat/components/PhoneSliderBar.tsx index d9e0498e5..1c42d698a 100644 --- a/src/pages/chat/components/PhoneSliderBar.tsx +++ b/src/pages/chat/components/PhoneSliderBar.tsx @@ -172,7 +172,7 @@ const PhoneSliderBar = ({ - router.push('/')}> + router.push('/model')}> <> 退出聊天 diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 85c41ec61..13c460eea 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -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(null); const TextareaDom = useRef(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; + }) => ( + + {AiDetail && chatData.model.canUse && history.obj === 'AI' && ( + router.push(`/model?modelId=${chatData.modelId}`)} + > + AI助手详情 + + )} + onclickCopy(history.value)}>复制 + {hasVoiceApi && ( + voiceBroadcast({ text: history.value })} + > + 语音播报 + + )} + + delChatRecord(index, history._id)}>删除 + + ), + [ + chatData.model.canUse, + chatData.modelId, + delChatRecord, + hasVoiceApi, + onclickCopy, + router, + theme.borders.base + ] + ); + return ( {/* pc always show history. phone is only show when modelId is present */} - {isPc || !modelId ? ( + {(isPc || !modelId) && ( - ) : ( - - - {chatData.model.name} - {chatId && ( + {!isPc && ( + + )} + router.push(`/model?modelId=${chatData.modelId}`)} + > + {chatData.model.name} {ChatModelMap[chatData.chatModel].name} + {chatData.history.length > 0 ? ` (${chatData.history.length})` : ''} + + {chatId ? ( onclickExportChat('md')}>导出Markdown格式 + ) : ( + )} - - - - - - - - )} - - {/* 聊天内容 */} - {modelId && ( - {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] })} > @@ -708,29 +759,12 @@ const Chat = ({ /> - {!isPc && ( - - {chatData.model.canUse && item.obj === 'AI' && ( - router.push(`/model?modelId=${chatData.modelId}`)} - > - AI助手详情 - - )} - onclickCopy(item.value)}>复制 - delChatRecord(index, item._id)}>删除 - - )} + {!isPc && } {/* message */} - + {item.obj === 'AI' ? ( - {isPc && ( - - {chatData.model.name} - - )} ) : ( - {isPc && ( - - Human - - )} )} + {/* phone slider */} + {!isPc && ( + + + + + + + )} {/* system prompt show modal */} { setShowSystemPrompt('')}> @@ -901,23 +939,13 @@ const Chat = ({ > - - onclickCopy(messageContextMenuData.message.value)}> - 复制 - - - delChatRecord( - chatData.history.findIndex( - (item) => item._id === messageContextMenuData.message._id - ), - messageContextMenuData.message._id - ) - } - > - 删除 - - + item._id === messageContextMenuData.message._id + )} + AiDetail={!isPc} + /> )} diff --git a/src/pages/model/components/detail/components/ModelEditForm.tsx b/src/pages/model/components/detail/components/ModelEditForm.tsx index bb4464461..b568cf351 100644 --- a/src/pages/model/components/detail/components/ModelEditForm.tsx +++ b/src/pages/model/components/detail/components/ModelEditForm.tsx @@ -145,7 +145,7 @@ const ModelEditForm = ({ {isOwner && ( - 删除AI和知识库 + 删除AI和知识库