feat: app ui
This commit is contained in:
@@ -45,6 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
jsonRes<InitShareChatResponse>(res, {
|
||||
data: {
|
||||
appId: shareChat.modelId,
|
||||
maxContext: shareChat.maxContext,
|
||||
userAvatar: user?.avatar || HUMAN_ICON,
|
||||
model: {
|
||||
|
||||
@@ -24,7 +24,7 @@ const API = ({ modelId }: { modelId: string }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Flex flexDirection={'column'} pt={[0, 5]} h={'100%'}>
|
||||
<Box display={['none', 'flex']} px={5} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
AppId:
|
||||
|
||||
@@ -161,7 +161,9 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
position={'relative'}
|
||||
maxW={['auto', '800px']}
|
||||
>
|
||||
<Box fontSize={['md', 'xl']}>基本信息</Box>
|
||||
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
|
||||
基本信息
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
头像
|
||||
|
||||
@@ -108,7 +108,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||
<Box position={'relative'} pt={[0, 5]} px={5} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
免登录聊天窗口
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Box, useOutsideClick, Flex, IconButton } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
|
||||
const ChatTest = ({
|
||||
app,
|
||||
modules,
|
||||
onClose
|
||||
}: {
|
||||
app: AppSchema;
|
||||
modules?: AppModuleItemType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const isOpen = useMemo(() => !!modules, [modules]);
|
||||
|
||||
const { ChatBox, ChatInput, ChatBoxParentRef, setChatHistory } = useChat({
|
||||
appId: app._id
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={isOpen ? '95%' : '0'}
|
||||
w={isOpen ? '460px' : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
>
|
||||
<Flex py={4} px={5} whiteSpace={'nowrap'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
调试预览
|
||||
</Box>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'clearLight'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setChatHistory([]);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box ref={ChatBoxParentRef} flex={1} px={5} overflow={'overlay'}>
|
||||
<ChatBox appAvatar={app.avatar} />
|
||||
</Box>
|
||||
|
||||
<Box px={5}>
|
||||
<ChatInput />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatTest;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
@@ -27,6 +27,7 @@ import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
||||
ssr: false
|
||||
});
|
||||
@@ -48,13 +49,16 @@ const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput')
|
||||
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
||||
ssr: false
|
||||
});
|
||||
const ChatTest = dynamic(() => import('./components/ChatTest'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
import { AppModuleTemplateItemType } from '@/types/app';
|
||||
import { AppModuleItemType, AppModuleTemplateItemType } from '@/types/app';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
@@ -83,6 +87,7 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
|
||||
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||
@@ -149,6 +154,41 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
},
|
||||
[onChangeNode, onDelNode, setNodes, x, y, zoom]
|
||||
);
|
||||
const flow2Modules = useCallback(() => {
|
||||
const modules: AppModuleItemType[] = nodes.map((item) => ({
|
||||
...item.data,
|
||||
position: item.position,
|
||||
onChangeNode: undefined,
|
||||
onDelNode: undefined,
|
||||
outputs: item.data.outputs.map((output) => ({
|
||||
...output,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected = !!edges.find(
|
||||
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||
);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId &&
|
||||
edge.sourceHandle === output.key &&
|
||||
edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
}, [edges, nodes]);
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
(id: string) => {
|
||||
@@ -177,42 +217,8 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
|
||||
const { mutate: onclickSave, isLoading } = useRequest({
|
||||
mutationFn: () => {
|
||||
const modules = nodes.map((item) => ({
|
||||
...item.data,
|
||||
position: item.position,
|
||||
onChangeNode: undefined,
|
||||
onDelNode: undefined,
|
||||
outputs: item.data.outputs.map((output) => ({
|
||||
...output,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
console.log(modules);
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected = !!edges.find(
|
||||
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||
);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId &&
|
||||
edge.sourceHandle === output.key &&
|
||||
edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
return putAppById(app._id, {
|
||||
modules
|
||||
modules: flow2Modules()
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
@@ -244,10 +250,11 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
initData(JSON.parse(JSON.stringify(app)));
|
||||
}, [app]);
|
||||
}, [app, initData]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
{/* header */}
|
||||
<Flex py={3} px={5} borderBottom={theme.borders.base} alignItems={'center'}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
@@ -263,20 +270,42 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
<Box ml={5} fontSize={'xl'} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
bg={'myBlue.200'}
|
||||
variant={'base'}
|
||||
border={'none'}
|
||||
color={'myGray.900'}
|
||||
_hover={{
|
||||
bg: 'myBlue.300'
|
||||
}}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
{testModules ? (
|
||||
<IconButton
|
||||
mr={6}
|
||||
icon={<SmallCloseIcon fontSize={'25px'} />}
|
||||
variant={'base'}
|
||||
color={'myGray.600'}
|
||||
borderRadius={'lg'}
|
||||
aria-label={''}
|
||||
onClick={() => setTestModules(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<MyTooltip label={'测试对话'}>
|
||||
<IconButton
|
||||
mr={6}
|
||||
icon={<MyIcon name={'chatLight'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
aria-label={'save'}
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
onclickSave();
|
||||
setTestModules(flow2Modules());
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
|
||||
<MyTooltip label={'保存配置'}>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
@@ -288,6 +317,7 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
@@ -334,7 +364,9 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
showInteractive={false}
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
|
||||
<ChatTest modules={testModules} app={app} onClose={() => setTestModules(undefined)} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import SlideTabs from '@/components/SlideTabs';
|
||||
import Settings from './components/Settings';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
|
||||
const EditApp = dynamic(() => import('./components/edit'), {
|
||||
@@ -54,11 +55,11 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
{ label: '基础信息', id: TabEnum.settings, icon: 'text' },
|
||||
...(isOwner ? [{ label: '编排', id: TabEnum.edit, icon: 'edit' }] : []),
|
||||
{ label: '分享', id: TabEnum.share, icon: 'edit' },
|
||||
{ label: 'API', id: TabEnum.API, icon: 'edit' },
|
||||
{ label: '立即对话', id: 'startChat', icon: 'chat' }
|
||||
{ label: '概览', id: TabEnum.settings, icon: 'overviewLight' },
|
||||
...(isOwner ? [{ label: '高级设置', id: TabEnum.edit, icon: 'settingLight' }] : []),
|
||||
{ label: '链接分享', id: TabEnum.share, icon: 'shareLight' },
|
||||
{ label: 'API访问', id: TabEnum.API, icon: 'apiLight' },
|
||||
{ label: '立即对话', id: 'startChat', icon: 'chatLight' }
|
||||
],
|
||||
[isOwner]
|
||||
);
|
||||
@@ -82,7 +83,13 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
<PageContainer>
|
||||
<Box display={['block', 'flex']} h={'100%'}>
|
||||
{/* pc tab */}
|
||||
<Box display={['none', 'block']} p={4} w={'200px'} borderRight={theme.borders.base}>
|
||||
<Box
|
||||
display={['none', 'flex']}
|
||||
flexDirection={'column'}
|
||||
p={4}
|
||||
w={'200px'}
|
||||
borderRight={theme.borders.base}
|
||||
>
|
||||
<Flex mb={4} alignItems={'center'}>
|
||||
<Avatar src={appDetail.avatar} w={'34px'} borderRadius={'lg'} />
|
||||
<Box ml={2} fontWeight={'bold'}>
|
||||
@@ -90,6 +97,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
</Box>
|
||||
</Flex>
|
||||
<SlideTabs
|
||||
flex={1}
|
||||
mx={'auto'}
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
@@ -103,6 +111,27 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
onClick={() => router.replace('/app/list')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
|
||||
bg={'white'}
|
||||
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||
h={'28px'}
|
||||
size={'sm'}
|
||||
borderRadius={'50%'}
|
||||
aria-label={''}
|
||||
/>
|
||||
我的应用
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* phone tab */}
|
||||
<Box display={['block', 'none']} textAlign={'center'} px={5} py={3}>
|
||||
|
||||
@@ -57,6 +57,7 @@ const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
|
||||
const textareaMinH = '22px';
|
||||
|
||||
@@ -65,16 +66,9 @@ const Chat = () => {
|
||||
const { shareId = '', historyId } = router.query as { shareId: string; historyId: string };
|
||||
const theme = useTheme();
|
||||
|
||||
const ChatBox = useRef<HTMLDivElement>(null);
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
const ContextMenuRef = useRef(null);
|
||||
const PhoneContextShow = useRef(false);
|
||||
|
||||
// 中断请求
|
||||
const controller = useRef(new AbortController());
|
||||
const isLeavePage = useRef(false);
|
||||
|
||||
const [inputVal, setInputVal] = useState(''); // user input prompt
|
||||
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
||||
// message messageContextMenuData
|
||||
left: number;
|
||||
@@ -99,6 +93,10 @@ const Chat = () => {
|
||||
[shareChatData.history]
|
||||
);
|
||||
|
||||
const { ChatBox, ChatInput, ChatBoxParentRef, setChatHistory, scrollToBottom } = useChat({
|
||||
appId: shareChatData.appId
|
||||
});
|
||||
|
||||
const { toast } = useToast();
|
||||
const { copyData } = useCopyData();
|
||||
const { isPc } = useGlobalStore();
|
||||
@@ -129,220 +127,6 @@ const Chat = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||
if (!ChatBox.current) return;
|
||||
ChatBox.current.scrollTo({
|
||||
top: ChatBox.current.scrollHeight,
|
||||
behavior
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const generatingMessage = useCallback(
|
||||
throttle(() => {
|
||||
if (!ChatBox.current) return;
|
||||
const isBottom =
|
||||
ChatBox.current.scrollTop + ChatBox.current.clientHeight + 150 >=
|
||||
ChatBox.current.scrollHeight;
|
||||
|
||||
isBottom && scrollToBottom('auto');
|
||||
}, 100),
|
||||
[]
|
||||
);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback((val: string) => {
|
||||
setInputVal(val);
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height =
|
||||
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// gpt 对话
|
||||
const gptChatPrompt = useCallback(
|
||||
async (prompts: ChatSiteItemType[]) => {
|
||||
// create abort obj
|
||||
const abortSignal = new AbortController();
|
||||
controller.current = abortSignal;
|
||||
isLeavePage.current = false;
|
||||
|
||||
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText } = await streamFetch({
|
||||
data: {
|
||||
messages: messages.slice(-shareChatData.maxContext - 1, -1),
|
||||
password,
|
||||
shareId,
|
||||
model: ''
|
||||
},
|
||||
onMessage: (text: string) => {
|
||||
setShareChatData((state) => ({
|
||||
...state,
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + text
|
||||
};
|
||||
})
|
||||
}));
|
||||
generatingMessage();
|
||||
},
|
||||
abortSignal
|
||||
});
|
||||
|
||||
// 重置了页面,说明退出了当前聊天, 不缓存任何内容
|
||||
if (isLeavePage.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let responseHistory: ChatSiteItemType[] = [];
|
||||
|
||||
// 设置聊天内容为完成状态
|
||||
setShareChatData((state) => {
|
||||
responseHistory = state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
history: responseHistory
|
||||
};
|
||||
});
|
||||
|
||||
setShareChatHistory({
|
||||
historyId,
|
||||
shareId,
|
||||
title: prompts[prompts.length - 2].value,
|
||||
latestChat: responseText,
|
||||
chats: responseHistory
|
||||
});
|
||||
|
||||
window.top?.postMessage(
|
||||
{
|
||||
type: 'shareChatFinish',
|
||||
data: {
|
||||
question: prompts[prompts.length - 2].value,
|
||||
answer: responseText
|
||||
}
|
||||
},
|
||||
'*'
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
generatingMessage();
|
||||
}, 100);
|
||||
},
|
||||
[
|
||||
generatingMessage,
|
||||
historyId,
|
||||
password,
|
||||
setShareChatData,
|
||||
setShareChatHistory,
|
||||
shareChatData.maxContext,
|
||||
shareId
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* 发送一个内容
|
||||
*/
|
||||
const sendPrompt = useCallback(async () => {
|
||||
if (isChatting) {
|
||||
toast({
|
||||
title: '正在聊天中...请等待结束',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const storeInput = inputVal;
|
||||
// 去除空行
|
||||
const val = inputVal.trim().replace(/\n\s*/g, '\n');
|
||||
|
||||
if (!val) {
|
||||
toast({
|
||||
title: '内容为空',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...shareChatData.history,
|
||||
{
|
||||
_id: String(new Types.ObjectId()),
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
_id: String(new Types.ObjectId()),
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setShareChatData((state) => ({
|
||||
...state,
|
||||
history: newChatList
|
||||
}));
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
await gptChatPrompt(newChatList);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
|
||||
resetInputVal(storeInput);
|
||||
|
||||
setShareChatData((state) => ({
|
||||
...state,
|
||||
history: newChatList.slice(0, newChatList.length - 2)
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
isChatting,
|
||||
inputVal,
|
||||
shareChatData.history,
|
||||
setShareChatData,
|
||||
resetInputVal,
|
||||
toast,
|
||||
scrollToBottom,
|
||||
gptChatPrompt
|
||||
]);
|
||||
|
||||
// 复制内容
|
||||
const onclickCopy = useCallback(
|
||||
(value: string) => {
|
||||
const val = value.replace(/\n+/g, '\n');
|
||||
copyData(val);
|
||||
},
|
||||
[copyData]
|
||||
);
|
||||
|
||||
// export chat data
|
||||
const onclickExportChat = useCallback(
|
||||
(type: ExportChatType) => {
|
||||
@@ -411,34 +195,6 @@ const Chat = () => {
|
||||
[shareChatData.history]
|
||||
);
|
||||
|
||||
// onclick chat message context
|
||||
const onclickContextMenu = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>, message: ChatSiteItemType) => {
|
||||
e.preventDefault(); // 阻止默认右键菜单
|
||||
|
||||
// select all text
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(e.currentTarget as HTMLDivElement);
|
||||
window.getSelection()?.removeAllRanges();
|
||||
window.getSelection()?.addRange(range);
|
||||
|
||||
navigator.vibrate?.(50); // 震动 50 毫秒
|
||||
|
||||
if (!isPc) {
|
||||
PhoneContextShow.current = true;
|
||||
}
|
||||
|
||||
setMessageContextMenuData({
|
||||
left: e.clientX - 20,
|
||||
top: e.clientY,
|
||||
message
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
[isPc]
|
||||
);
|
||||
|
||||
// 获取对话信息
|
||||
const loadChatInfo = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@@ -503,41 +259,8 @@ const Chat = () => {
|
||||
return loadChatInfo();
|
||||
});
|
||||
|
||||
// abort stream
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.speechSynthesis?.cancel();
|
||||
isLeavePage.current = true;
|
||||
controller.current?.abort();
|
||||
};
|
||||
}, [shareId, historyId]);
|
||||
|
||||
// context menu component
|
||||
const RenderContextMenu = useCallback(
|
||||
({ history, index }: { history: ChatSiteItemType; index: number }) => (
|
||||
<MenuList fontSize={'sm'} minW={'100px !important'}>
|
||||
<MenuItem onClick={() => onclickCopy(history.value)}>复制</MenuItem>
|
||||
{hasVoiceApi && (
|
||||
<MenuItem
|
||||
borderBottom={theme.borders.base}
|
||||
onClick={() => voiceBroadcast({ text: history.value })}
|
||||
>
|
||||
语音播报
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem onClick={() => delShareChatHistoryItemById(historyId, index)}>删除</MenuItem>
|
||||
</MenuList>
|
||||
),
|
||||
[delShareChatHistoryItemById, historyId, onclickCopy, theme.borders.base]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
h={'100%'}
|
||||
flexDirection={['column', 'row']}
|
||||
backgroundColor={useColorModeValue('#fdfdfd', '')}
|
||||
>
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}>
|
||||
{/* pc always show history. */}
|
||||
{isPc && (
|
||||
<SideBar>
|
||||
@@ -612,164 +335,11 @@ const Chat = () => {
|
||||
)}
|
||||
</Flex>
|
||||
{/* chat content box */}
|
||||
<Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'}>
|
||||
<Box id={'history'}>
|
||||
{shareChatData.history.map((item, index) => (
|
||||
<Flex key={item._id} alignItems={'flex-start'} py={2} px={[2, 6, 8]}>
|
||||
{item.obj === 'Human' && <Box flex={1} />}
|
||||
{/* avatar */}
|
||||
<Menu autoSelect={false} isLazy>
|
||||
<MyTooltip label={item.obj === 'AI' ? '应用详情' : ''}>
|
||||
<MenuButton
|
||||
as={Box}
|
||||
{...(item.obj === 'AI'
|
||||
? {
|
||||
order: 1,
|
||||
mr: ['6px', 2]
|
||||
}
|
||||
: {
|
||||
order: 3,
|
||||
ml: ['6px', 2]
|
||||
})}
|
||||
>
|
||||
<Avatar
|
||||
src={
|
||||
item.obj === 'Human'
|
||||
? shareChatData.userAvatar || HUMAN_ICON
|
||||
: shareChatData.model.avatar
|
||||
}
|
||||
w={['20px', '34px']}
|
||||
h={['20px', '34px']}
|
||||
/>
|
||||
</MenuButton>
|
||||
</MyTooltip>
|
||||
{!isPc && <RenderContextMenu history={item} index={index} />}
|
||||
</Menu>
|
||||
{/* message */}
|
||||
<Flex order={2} pt={2} maxW={['calc(100% - 50px)', '80%']}>
|
||||
{item.obj === 'AI' ? (
|
||||
<Box w={'100%'}>
|
||||
<Card
|
||||
bg={'white'}
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'0 8px 8px 8px'}
|
||||
onContextMenu={(e) => onclickContextMenu(e, item)}
|
||||
>
|
||||
<Markdown
|
||||
source={item.value}
|
||||
isChatting={isChatting && index === shareChatData.history.length - 1}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Card
|
||||
className="markdown"
|
||||
whiteSpace={'pre-wrap'}
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'8px 0 8px 8px'}
|
||||
bg={'myBlue.300'}
|
||||
onContextMenu={(e) => onclickContextMenu(e, item)}
|
||||
>
|
||||
<Box as={'p'}>{item.value}</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
{shareChatData.history.length === 0 && (
|
||||
<Empty model={shareChatData.model} showChatProblem={false} />
|
||||
)}
|
||||
</Box>
|
||||
<Box ref={ChatBoxParentRef} flex={1}>
|
||||
<ChatBox appAvatar={shareChatData.model.avatar} />
|
||||
</Box>
|
||||
{/* 发送区 */}
|
||||
<Box m={['0 auto', '20px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']}>
|
||||
<Box
|
||||
py={'18px'}
|
||||
position={'relative'}
|
||||
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
|
||||
borderTop={['1px solid', 0]}
|
||||
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
borderRadius={['none', 'md']}
|
||||
backgroundColor={useColorModeValue('white', 'gray.700')}
|
||||
>
|
||||
{/* 输入框 */}
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
py={0}
|
||||
pr={['45px', '55px']}
|
||||
border={'none'}
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
placeholder="提问"
|
||||
resize={'none'}
|
||||
value={inputVal}
|
||||
rows={1}
|
||||
height={'22px'}
|
||||
lineHeight={'22px'}
|
||||
maxHeight={'150px'}
|
||||
maxLength={-1}
|
||||
overflowY={'auto'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
boxShadow={'none !important'}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
setInputVal(textarea.value);
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (isPc && e.keyCode === 13 && !e.shiftKey) {
|
||||
sendPrompt();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
}}
|
||||
/>
|
||||
{/* 发送和等待按键 */}
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
h={'25px'}
|
||||
w={'25px'}
|
||||
position={'absolute'}
|
||||
right={['12px', '20px']}
|
||||
bottom={'15px'}
|
||||
>
|
||||
{isChatting ? (
|
||||
<MyIcon
|
||||
className={styles.stopIcon}
|
||||
width={['22px', '25px']}
|
||||
height={['22px', '25px']}
|
||||
cursor={'pointer'}
|
||||
name={'stop'}
|
||||
color={useColorModeValue('gray.500', 'white')}
|
||||
onClick={() => {
|
||||
controller.current?.abort();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MyIcon
|
||||
name={'chatSend'}
|
||||
width={['18px', '20px']}
|
||||
height={['18px', '20px']}
|
||||
cursor={'pointer'}
|
||||
color={useColorModeValue('gray.500', 'white')}
|
||||
onClick={sendPrompt}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<ChatInput />
|
||||
|
||||
<Loading fixed={false} />
|
||||
</Flex>
|
||||
@@ -787,25 +357,6 @@ const Chat = () => {
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
{/* context menu */}
|
||||
{messageContextMenuData && (
|
||||
<Box
|
||||
zIndex={10}
|
||||
position={'fixed'}
|
||||
top={messageContextMenuData.top}
|
||||
left={messageContextMenuData.left}
|
||||
>
|
||||
<Box ref={ContextMenuRef}></Box>
|
||||
<Menu isOpen>
|
||||
<RenderContextMenu
|
||||
history={messageContextMenuData.message}
|
||||
index={shareChatData.history.findIndex(
|
||||
(item) => item._id === messageContextMenuData.message._id
|
||||
)}
|
||||
/>
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
{/* password input */}
|
||||
{
|
||||
<Modal isOpen={isOpenPassword} onClose={onClosePassword}>
|
||||
|
||||
Reference in New Issue
Block a user