diff --git a/client/src/api/response/chat.d.ts b/client/src/api/response/chat.d.ts
index 7b458c07a..6ce97c30c 100644
--- a/client/src/api/response/chat.d.ts
+++ b/client/src/api/response/chat.d.ts
@@ -19,6 +19,7 @@ export interface InitChatResponse {
export interface InitShareChatResponse {
maxContext: number;
userAvatar: string;
+ appId: string;
model: {
name: string;
avatar: string;
diff --git a/client/src/components/Icon/icons/appStore.svg b/client/src/components/Icon/icons/fill/app.svg
similarity index 100%
rename from client/src/components/Icon/icons/appStore.svg
rename to client/src/components/Icon/icons/fill/app.svg
diff --git a/client/src/components/Icon/icons/fill/back.svg b/client/src/components/Icon/icons/fill/back.svg
new file mode 100644
index 000000000..5064057b0
--- /dev/null
+++ b/client/src/components/Icon/icons/fill/back.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/fill/chat.svg b/client/src/components/Icon/icons/fill/chat.svg
new file mode 100644
index 000000000..042a434f5
--- /dev/null
+++ b/client/src/components/Icon/icons/fill/chat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/fill/db.svg b/client/src/components/Icon/icons/fill/db.svg
new file mode 100644
index 000000000..0cba56612
--- /dev/null
+++ b/client/src/components/Icon/icons/fill/db.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/fill/me.svg b/client/src/components/Icon/icons/fill/me.svg
new file mode 100644
index 000000000..e9f4226c5
--- /dev/null
+++ b/client/src/components/Icon/icons/fill/me.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/git.svg b/client/src/components/Icon/icons/git.svg
index 665bda378..dc921e04d 100644
--- a/client/src/components/Icon/icons/git.svg
+++ b/client/src/components/Icon/icons/git.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/kb.svg b/client/src/components/Icon/icons/kb.svg
deleted file mode 100644
index b5b7e8e86..000000000
--- a/client/src/components/Icon/icons/kb.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/app.svg b/client/src/components/Icon/icons/light/app.svg
new file mode 100644
index 000000000..852db438a
--- /dev/null
+++ b/client/src/components/Icon/icons/light/app.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/appApi.svg b/client/src/components/Icon/icons/light/appApi.svg
new file mode 100644
index 000000000..e906d9b3a
--- /dev/null
+++ b/client/src/components/Icon/icons/light/appApi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/chat.svg b/client/src/components/Icon/icons/light/chat.svg
index c2561573d..314547a0e 100644
--- a/client/src/components/Icon/icons/light/chat.svg
+++ b/client/src/components/Icon/icons/light/chat.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/clear.svg b/client/src/components/Icon/icons/light/clear.svg
new file mode 100644
index 000000000..06515cad5
--- /dev/null
+++ b/client/src/components/Icon/icons/light/clear.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/db.svg b/client/src/components/Icon/icons/light/db.svg
new file mode 100644
index 000000000..b784f35dd
--- /dev/null
+++ b/client/src/components/Icon/icons/light/db.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/me.svg b/client/src/components/Icon/icons/light/me.svg
new file mode 100644
index 000000000..9147dba37
--- /dev/null
+++ b/client/src/components/Icon/icons/light/me.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/overview.svg b/client/src/components/Icon/icons/light/overview.svg
new file mode 100644
index 000000000..fcfb64a38
--- /dev/null
+++ b/client/src/components/Icon/icons/light/overview.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/setting.svg b/client/src/components/Icon/icons/light/setting.svg
new file mode 100644
index 000000000..a4de19777
--- /dev/null
+++ b/client/src/components/Icon/icons/light/setting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/icons/light/share.svg b/client/src/components/Icon/icons/light/share.svg
new file mode 100644
index 000000000..414ae060c
--- /dev/null
+++ b/client/src/components/Icon/icons/light/share.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx
index 895fe49cf..61f5f2181 100644
--- a/client/src/components/Icon/index.tsx
+++ b/client/src/components/Icon/index.tsx
@@ -6,15 +6,14 @@ const map = {
model: require('./icons/model.svg').default,
copy: require('./icons/copy.svg').default,
chatSend: require('./icons/chatSend.svg').default,
- user: require('./icons/user.svg').default,
delete: require('./icons/delete.svg').default,
withdraw: require('./icons/withdraw.svg').default,
stop: require('./icons/stop.svg').default,
collectionLight: require('./icons/collectionLight.svg').default,
collectionSolid: require('./icons/collectionSolid.svg').default,
- chat: require('./icons/chat.svg').default,
empty: require('./icons/empty.svg').default,
back: require('./icons/back.svg').default,
+ backFill: require('./icons/fill/back.svg').default,
more: require('./icons/more.svg').default,
tabbarChat: require('./icons/phoneTabbar/chat.svg').default,
tabbarModel: require('./icons/phoneTabbar/model.svg').default,
@@ -24,8 +23,6 @@ const map = {
wx: require('./icons/wx.svg').default,
out: require('./icons/out.svg').default,
git: require('./icons/git.svg').default,
- kb: require('./icons/kb.svg').default,
- appStore: require('./icons/appStore.svg').default,
menu: require('./icons/menu.svg').default,
edit: require('./icons/edit.svg').default,
inform: require('./icons/inform.svg').default,
@@ -37,7 +34,19 @@ const map = {
apikey: require('./icons/apikey.svg').default,
save: require('./icons/save.svg').default,
minus: require('./icons/minus.svg').default,
- chatLight: require('./icons/light/chat.svg').default
+ chatLight: require('./icons/light/chat.svg').default,
+ chatFill: require('./icons/fill/chat.svg').default,
+ clearLight: require('./icons/light/clear.svg').default,
+ apiLight: require('./icons/light/appApi.svg').default,
+ overviewLight: require('./icons/light/overview.svg').default,
+ settingLight: require('./icons/light/setting.svg').default,
+ shareLight: require('./icons/light/share.svg').default,
+ dbLight: require('./icons/light/db.svg').default,
+ dbFill: require('./icons/fill/db.svg').default,
+ appLight: require('./icons/light/app.svg').default,
+ appFill: require('./icons/fill/app.svg').default,
+ meLight: require('./icons/light/me.svg').default,
+ meFill: require('./icons/fill/me.svg').default
};
export type IconName = keyof typeof map;
diff --git a/client/src/components/Layout/index.tsx b/client/src/components/Layout/index.tsx
index 27f3f9601..803f99831 100644
--- a/client/src/components/Layout/index.tsx
+++ b/client/src/components/Layout/index.tsx
@@ -61,16 +61,16 @@ const Layout = ({ children }: { children: JSX.Element }) => {
return (
<>
-
+
{pcUnShowLayoutRoute[router.pathname] ? (
{children}
) : (
<>
-
+
-
+
{children}
>
diff --git a/client/src/components/Layout/navbar.tsx b/client/src/components/Layout/navbar.tsx
index f22d1f24a..dbd0b6007 100644
--- a/client/src/components/Layout/navbar.tsx
+++ b/client/src/components/Layout/navbar.tsx
@@ -22,31 +22,36 @@ const Navbar = ({ unread }: { unread: number }) => {
() => [
{
label: '聊天',
- icon: 'chat',
+ icon: 'chatLight',
+ activeIcon: 'chatFill',
link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`,
activeLink: ['/chat']
},
{
label: '应用',
- icon: 'model',
+ icon: 'tabbarModel',
+ activeIcon: 'model',
link: `/app/list`,
activeLink: ['/app/list', '/app/detail']
},
{
label: '知识库',
- icon: 'kb',
+ icon: 'dbLight',
+ activeIcon: 'dbFill',
link: `/kb`,
activeLink: ['/kb']
},
{
label: '市场',
- icon: 'appStore',
+ icon: 'appLight',
+ activeIcon: 'appFill',
link: '/appStore',
activeLink: ['/appStore']
},
{
label: '账号',
- icon: 'user',
+ icon: 'meLight',
+ activeIcon: 'meFill',
link: '/number',
activeLink: ['/number']
}
@@ -65,7 +70,7 @@ const Navbar = ({ unread }: { unread: number }) => {
h: '54px',
borderRadius: 'md',
_hover: {
- color: '#ffffff'
+ bg: 'myWhite.600'
}
};
@@ -74,17 +79,17 @@ const Navbar = ({ unread }: { unread: number }) => {
flexDirection={'column'}
alignItems={'center'}
pt={6}
- backgroundImage={'linear-gradient(to bottom right,#465069,#000000)'}
+ bg={'white'}
h={'100%'}
w={'100%'}
- boxShadow={'4px 0px 4px 0px rgba(43, 45, 55, 0.01)'}
+ boxShadow={'2px 0px 8px 0px rgba(0,0,0,0.1)'}
userSelect={'none'}
>
{/* logo */}
router.push('/number')}
@@ -101,15 +106,24 @@ const Navbar = ({ unread }: { unread: number }) => {
{...itemStyles}
{...(item.activeLink.includes(router.pathname)
? {
- color: '#ffffff ',
- backgroundImage: 'linear-gradient(to bottom right, #2152d9 0%, #4e83fd 100%)'
+ color: 'myBlue.700',
+ bg: 'white !important',
+ boxShadow: '1px 1px 10px rgba(0,0,0,0.2)'
}
: {
- color: '#9096a5',
+ color: 'myGray.500',
backgroundColor: 'transparent'
})}
>
-
+
{item.label}
diff --git a/client/src/components/PageContainer/index.tsx b/client/src/components/PageContainer/index.tsx
index 0745de257..8959e0516 100644
--- a/client/src/components/PageContainer/index.tsx
+++ b/client/src/components/PageContainer/index.tsx
@@ -4,7 +4,7 @@ import { Box, useTheme, type BoxProps } from '@chakra-ui/react';
const PageContainer = ({ children, ...props }: BoxProps) => {
const theme = useTheme();
return (
-
+
-
+
{item.label}
))}
diff --git a/client/src/hooks/useChat.module.scss b/client/src/hooks/useChat.module.scss
new file mode 100644
index 000000000..477d13f08
--- /dev/null
+++ b/client/src/hooks/useChat.module.scss
@@ -0,0 +1,30 @@
+.stopIcon {
+ animation: zoomStopIcon 0.4s infinite alternate;
+}
+@keyframes zoomStopIcon {
+ 0% {
+ transform: scale(0.8);
+ }
+ 100% {
+ transform: scale(1.2);
+ }
+}
+
+.newChat {
+ .modelListContainer {
+ height: 0;
+ overflow: hidden;
+ }
+ .modelList {
+ border-radius: 6px;
+ }
+ &:hover {
+ .modelListContainer {
+ height: 60vh;
+ }
+ .modelList {
+ box-shadow: 0 0 5px rgba($color: #000000, $alpha: 0.05);
+ border: 1px solid #dee0e2;
+ }
+ }
+}
diff --git a/client/src/hooks/useChat.tsx b/client/src/hooks/useChat.tsx
new file mode 100644
index 000000000..9ef982f8f
--- /dev/null
+++ b/client/src/hooks/useChat.tsx
@@ -0,0 +1,367 @@
+import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
+import { throttle } from 'lodash';
+import { ChatSiteItemType } from '@/types/chat';
+import { useToast } from './useToast';
+import { useCopyData } from '@/utils/tools';
+import { Box, Card, Flex, Textarea } from '@chakra-ui/react';
+import { useUserStore } from '@/store/user';
+import { useRouter } from 'next/router';
+
+import { Types } from 'mongoose';
+import { HUMAN_ICON } from '@/constants/chat';
+import Markdown from '@/components/Markdown';
+import MyIcon from '@/components/Icon';
+import Avatar from '@/components/Avatar';
+
+import styles from './useChat.module.scss';
+import { adaptChatItem_openAI } from '@/utils/plugin/openai';
+import { streamFetch } from '@/api/fetch';
+
+const textareaMinH = '22px';
+
+export const useChat = ({ appId }: { appId: string }) => {
+ const router = useRouter();
+ const ChatBoxParentRef = useRef(null);
+ const TextareaDom = useRef(null);
+
+ // stop chat
+ const controller = useRef(new AbortController());
+ const isLeavePage = useRef(false);
+
+ const [chatHistory, setChatHistory] = useState([]);
+ const { toast } = useToast();
+ const { copyData } = useCopyData();
+ const { userInfo } = useUserStore();
+
+ const isChatting = useMemo(
+ () => chatHistory[chatHistory.length - 1]?.status === 'loading',
+ [chatHistory]
+ );
+ const isLargeWidth =
+ ChatBoxParentRef?.current?.clientWidth && ChatBoxParentRef?.current?.clientWidth > 900;
+
+ // 滚动到底部
+ const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
+ if (!ChatBoxParentRef.current) return;
+ console.log(ChatBoxParentRef.current.scrollHeight);
+
+ ChatBoxParentRef.current.scrollTo({
+ top: ChatBoxParentRef.current.scrollHeight,
+ behavior
+ });
+ }, []);
+
+ // 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const generatingMessage = useCallback(
+ throttle(() => {
+ if (!ChatBoxParentRef.current) return;
+ const isBottom =
+ ChatBoxParentRef.current.scrollTop + ChatBoxParentRef.current.clientHeight + 150 >=
+ ChatBoxParentRef.current.scrollHeight;
+
+ isBottom && scrollToBottom('auto');
+ }, 100),
+ []
+ );
+
+ // 复制内容
+ const onclickCopy = useCallback(
+ (value: string) => {
+ const val = value.replace(/\n+/g, '\n');
+ copyData(val);
+ },
+ [copyData]
+ );
+
+ // 重置输入内容
+ const resetInputVal = useCallback((val: string) => {
+ if (!TextareaDom.current) return;
+ TextareaDom.current.value = val;
+ setTimeout(() => {
+ /* 回到最小高度 */
+ if (TextareaDom.current) {
+ TextareaDom.current.style.height =
+ val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
+ }
+ }, 100);
+ }, []);
+
+ const startChat = 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 });
+
+ // 流请求,获取数据
+ await streamFetch({
+ data: {
+ messages,
+ appId,
+ model: ''
+ },
+ onMessage: (text: string) => {
+ setChatHistory((state) =>
+ state.map((item, index) => {
+ if (index !== state.length - 1) return item;
+ return {
+ ...item,
+ value: item.value + text
+ };
+ })
+ );
+ generatingMessage();
+ },
+ abortSignal
+ });
+
+ // 重置了页面,说明退出了当前聊天, 不缓存任何内容
+ if (isLeavePage.current) {
+ return;
+ }
+
+ // 设置聊天内容为完成状态
+ setChatHistory((state) =>
+ state.map((item, index) => {
+ if (index !== state.length - 1) return item;
+ return {
+ ...item,
+ status: 'finish'
+ };
+ })
+ );
+
+ setTimeout(() => {
+ generatingMessage();
+ TextareaDom.current?.focus();
+ }, 100);
+ },
+ [appId, generatingMessage]
+ );
+
+ /**
+ * user confirm send prompt
+ */
+ const sendPrompt = useCallback(async () => {
+ if (isChatting) {
+ toast({
+ title: '正在聊天中...请等待结束',
+ status: 'warning'
+ });
+ return;
+ }
+ // get input value
+ const value = TextareaDom.current?.value || '';
+ const val = value.trim().replace(/\n\s*/g, '\n');
+
+ if (!val) {
+ toast({
+ title: '内容为空',
+ status: 'warning'
+ });
+ return;
+ }
+
+ const newChatList: ChatSiteItemType[] = [
+ ...chatHistory,
+ {
+ _id: String(new Types.ObjectId()),
+ obj: 'Human',
+ value: val,
+ status: 'finish'
+ },
+ {
+ _id: String(new Types.ObjectId()),
+ obj: 'AI',
+ value: '',
+ status: 'loading'
+ }
+ ];
+
+ // 插入内容
+ setChatHistory(newChatList);
+
+ // 清空输入内容
+ resetInputVal('');
+ setTimeout(() => {
+ scrollToBottom();
+ }, 100);
+
+ try {
+ await startChat(newChatList);
+ } catch (err: any) {
+ toast({
+ title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
+ status: 'warning',
+ duration: 5000,
+ isClosable: true
+ });
+
+ resetInputVal(value);
+
+ setChatHistory(newChatList.slice(0, newChatList.length - 2));
+ }
+ }, [isChatting, chatHistory, resetInputVal, toast, scrollToBottom, startChat]);
+
+ const ChatBox = useCallback(
+ ({ appAvatar }: { appAvatar: string }) => {
+ return (
+
+ {chatHistory.map((item, index) => (
+
+ {item.obj === 'Human' && }
+ {/* avatar */}
+
+ {/* message */}
+
+ {item.obj === 'AI' ? (
+
+
+
+
+
+ ) : (
+
+
+ {item.value}
+
+
+ )}
+
+
+ ))}
+
+ );
+ },
+ [chatHistory, isChatting, userInfo?.avatar]
+ );
+
+ const ChatInput = useCallback(() => {
+ return (
+
+
+ {/* 输入框 */}
+
+
+ );
+ }, [isChatting, sendPrompt]);
+
+ // abort stream
+ useEffect(() => {
+ return () => {
+ window.speechSynthesis?.cancel();
+ isLeavePage.current = true;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ controller.current?.abort();
+ };
+ }, [router.asPath]);
+
+ return {
+ ChatBoxParentRef,
+ scrollToBottom,
+ setChatHistory,
+ ChatBox,
+ ChatInput
+ };
+};
diff --git a/client/src/pages/api/chat/shareChat/init.ts b/client/src/pages/api/chat/shareChat/init.ts
index 124ebf816..fc1e4ca4e 100644
--- a/client/src/pages/api/chat/shareChat/init.ts
+++ b/client/src/pages/api/chat/shareChat/init.ts
@@ -45,6 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
jsonRes(res, {
data: {
+ appId: shareChat.modelId,
maxContext: shareChat.maxContext,
userAvatar: user?.avatar || HUMAN_ICON,
model: {
diff --git a/client/src/pages/app/detail/components/API.tsx b/client/src/pages/app/detail/components/API.tsx
index 0b805112e..1365c0517 100644
--- a/client/src/pages/app/detail/components/API.tsx
+++ b/client/src/pages/app/detail/components/API.tsx
@@ -24,7 +24,7 @@ const API = ({ modelId }: { modelId: string }) => {
}, []);
return (
-
+
AppId:
diff --git a/client/src/pages/app/detail/components/Settings.tsx b/client/src/pages/app/detail/components/Settings.tsx
index e3c0ba1f1..58fbf5a7c 100644
--- a/client/src/pages/app/detail/components/Settings.tsx
+++ b/client/src/pages/app/detail/components/Settings.tsx
@@ -161,7 +161,9 @@ const Settings = ({ modelId }: { modelId: string }) => {
position={'relative'}
maxW={['auto', '800px']}
>
- 基本信息
+
+ 基本信息
+
头像
diff --git a/client/src/pages/app/detail/components/Share.tsx b/client/src/pages/app/detail/components/Share.tsx
index 112e706c4..9f7badd87 100644
--- a/client/src/pages/app/detail/components/Share.tsx
+++ b/client/src/pages/app/detail/components/Share.tsx
@@ -108,7 +108,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
};
return (
-
+
免登录聊天窗口
diff --git a/client/src/pages/app/detail/components/edit/components/ChatTest.tsx b/client/src/pages/app/detail/components/edit/components/ChatTest.tsx
new file mode 100644
index 000000000..869d0ac73
--- /dev/null
+++ b/client/src/pages/app/detail/components/edit/components/ChatTest.tsx
@@ -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 (
+
+
+
+ 调试预览
+
+ }
+ variant={'base'}
+ borderRadius={'md'}
+ aria-label={'delete'}
+ onClick={(e) => {
+ e.stopPropagation();
+ setChatHistory([]);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ChatTest;
diff --git a/client/src/pages/app/detail/components/edit/index.tsx b/client/src/pages/app/detail/components/edit/index.tsx
index 1c62b1ca4..9f9876135 100644
--- a/client/src/pages/app/detail/components/edit/index.tsx
+++ b/client/src/pages/app/detail/components/edit/index.tsx
@@ -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();
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 (
+ {/* header */}
{
{app.name}
- }
- borderRadius={'lg'}
- isLoading={isLoading}
- aria-label={'save'}
- bg={'myBlue.200'}
- variant={'base'}
- border={'none'}
- color={'myGray.900'}
- _hover={{
- bg: 'myBlue.300'
- }}
- onClick={onclickSave}
- />
+ {testModules ? (
+ }
+ variant={'base'}
+ color={'myGray.600'}
+ borderRadius={'lg'}
+ aria-label={''}
+ onClick={() => setTestModules(undefined)}
+ />
+ ) : (
+
+ }
+ borderRadius={'lg'}
+ aria-label={'save'}
+ variant={'base'}
+ onClick={() => {
+ // @ts-ignore
+ onclickSave();
+ setTestModules(flow2Modules());
+ }}
+ />
+
+ )}
+
+
+ }
+ borderRadius={'lg'}
+ isLoading={isLoading}
+ aria-label={'save'}
+ onClick={onclickSave}
+ />
+
{
return false;
}}
>
+ {/* open module template */}
{
showInteractive={false}
/>
+
+ setTestModules(undefined)} />
);
diff --git a/client/src/pages/app/detail/index.tsx b/client/src/pages/app/detail/index.tsx
index 4142c1f37..7024906b7 100644
--- a/client/src/pages/app/detail/index.tsx
+++ b/client/src/pages/app/detail/index.tsx
@@ -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}` }) => {
{/* pc tab */}
-
+
@@ -90,6 +97,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
{
}
}}
/>
+ router.replace('/app/list')}
+ >
+ }
+ bg={'white'}
+ boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
+ h={'28px'}
+ size={'sm'}
+ borderRadius={'50%'}
+ aria-label={''}
+ />
+ 我的应用
+
{/* phone tab */}
diff --git a/client/src/pages/chat/share.tsx b/client/src/pages/chat/share.tsx
index 5ec1f2a9e..4d5e63b3f 100644
--- a/client/src/pages/chat/share.tsx
+++ b/client/src/pages/chat/share.tsx
@@ -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(null);
- const TextareaDom = useRef(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, 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 }) => (
-
-
- {hasVoiceApi && (
-
- )}
-
-
-
- ),
- [delShareChatHistoryItemById, historyId, onclickCopy, theme.borders.base]
- );
-
return (
-
+
{/* pc always show history. */}
{isPc && (
@@ -612,164 +335,11 @@ const Chat = () => {
)}
{/* chat content box */}
-
-
- {shareChatData.history.map((item, index) => (
-
- {item.obj === 'Human' && }
- {/* avatar */}
-
- {/* message */}
-
- {item.obj === 'AI' ? (
-
- onclickContextMenu(e, item)}
- >
-
-
-
- ) : (
-
- onclickContextMenu(e, item)}
- >
- {item.value}
-
-
- )}
-
-
- ))}
- {shareChatData.history.length === 0 && (
-
- )}
-
+
+
{/* 发送区 */}
-
-
- {/* 输入框 */}
-
-
+
@@ -787,25 +357,6 @@ const Chat = () => {
)}
- {/* context menu */}
- {messageContextMenuData && (
-
-
-
-
- )}
{/* password input */}
{
diff --git a/client/src/store/chat.ts b/client/src/store/chat.ts
index cefcfccdf..2e8833077 100644
--- a/client/src/store/chat.ts
+++ b/client/src/store/chat.ts
@@ -57,6 +57,7 @@ const defaultChatData: ChatType = {
history: []
};
const defaultShareChatData: ShareChatType = {
+ appId: '',
maxContext: 5,
userAvatar: HUMAN_ICON,
model: {