chat box
This commit is contained in:
77
client/src/pages/api/chat/chatTest.ts
Normal file
77
client/src/pages/api/chat/chatTest.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { sseErrRes } from '@/service/response';
|
||||
import { sseResponseEventEnum } from '@/constants/chat';
|
||||
import { sseResponse } from '@/service/utils/tools';
|
||||
import { type ChatCompletionRequestMessage } from 'openai';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { dispatchModules } from '../openapi/v1/chat/completions2';
|
||||
|
||||
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
|
||||
export type Props = {
|
||||
history: MessageItemType[];
|
||||
prompt: string;
|
||||
modules: AppModuleItemType[];
|
||||
variable: Record<string, any>;
|
||||
};
|
||||
export type ChatResponseType = {
|
||||
newChatId: string;
|
||||
quoteLen?: number;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.on('close', () => {
|
||||
res.end();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
res.end();
|
||||
});
|
||||
|
||||
let { modules = [], history = [], prompt, variable = {} } = req.body as Props;
|
||||
|
||||
try {
|
||||
if (!history || !modules || !prompt) {
|
||||
throw new Error('Prams Error');
|
||||
}
|
||||
if (!Array.isArray(modules)) {
|
||||
throw new Error('history is not array');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
/* user auth */
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
/* start process */
|
||||
const { responseData } = await dispatchModules({
|
||||
res,
|
||||
modules: modules,
|
||||
variable,
|
||||
params: {
|
||||
history,
|
||||
userChatInput: prompt
|
||||
},
|
||||
stream: true
|
||||
});
|
||||
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: '[DONE]'
|
||||
});
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.appStreamResponse,
|
||||
data: JSON.stringify(responseData)
|
||||
});
|
||||
res.end();
|
||||
|
||||
// bill
|
||||
} catch (err: any) {
|
||||
res.status(500);
|
||||
sseErrRes(res, err);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
@@ -3,33 +3,36 @@ import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ShareChat } from '@/service/mongo';
|
||||
import { authApp, authUser } from '@/service/utils/auth';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||
|
||||
/* create a shareChat */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { modelId, name, maxContext, password } = req.body as ShareChatEditType & {
|
||||
modelId: string;
|
||||
const { appId, name, maxContext } = req.body as ShareChatEditType & {
|
||||
appId: string;
|
||||
};
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
await authApp({
|
||||
appId: modelId,
|
||||
appId,
|
||||
userId,
|
||||
authOwner: false
|
||||
});
|
||||
|
||||
const { _id } = await ShareChat.create({
|
||||
const shareId = nanoid();
|
||||
await ShareChat.create({
|
||||
shareId,
|
||||
userId,
|
||||
modelId,
|
||||
appId,
|
||||
name,
|
||||
password,
|
||||
maxContext
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: _id
|
||||
data: shareId
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
|
||||
@@ -5,13 +5,14 @@ import type { InitShareChatResponse } from '@/api/response/chat';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
import { hashPassword } from '@/service/utils/tools';
|
||||
import { HUMAN_ICON } from '@/constants/chat';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
let { shareId, password = '' } = req.query as {
|
||||
let { shareId } = req.query as {
|
||||
shareId: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
if (!shareId) {
|
||||
@@ -21,22 +22,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
|
||||
// get shareChat
|
||||
const shareChat = await ShareChat.findById(shareId);
|
||||
const shareChat = await ShareChat.findOne({ shareId });
|
||||
|
||||
if (!shareChat) {
|
||||
throw new Error('分享链接已失效');
|
||||
}
|
||||
|
||||
if (shareChat.password !== hashPassword(password)) {
|
||||
return jsonRes(res, {
|
||||
code: 501,
|
||||
message: '密码不正确'
|
||||
error: '分享链接已失效'
|
||||
});
|
||||
}
|
||||
|
||||
// 校验使用权限
|
||||
const { app } = await authApp({
|
||||
appId: shareChat.modelId,
|
||||
appId: shareChat.appId,
|
||||
userId: String(shareChat.userId),
|
||||
authOwner: false
|
||||
});
|
||||
@@ -45,15 +42,22 @@ 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: {
|
||||
maxContext:
|
||||
app.modules
|
||||
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
|
||||
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0,
|
||||
app: {
|
||||
variableModules: app.modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs?.find((item) => item.key === SystemInputEnum.variables)?.value,
|
||||
welcomeText: app.modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs?.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro
|
||||
},
|
||||
chatModel: app.chat.chatModel
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,11 +4,11 @@ import { connectToDatabase, ShareChat } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { hashPassword } from '@/service/utils/tools';
|
||||
|
||||
/* get shareChat list by modelId */
|
||||
/* get shareChat list by appId */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { modelId } = req.query as {
|
||||
modelId: string;
|
||||
const { appId } = req.query as {
|
||||
appId: string;
|
||||
};
|
||||
|
||||
await connectToDatabase();
|
||||
@@ -16,19 +16,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const data = await ShareChat.find({
|
||||
modelId,
|
||||
appId,
|
||||
userId
|
||||
}).sort({
|
||||
_id: -1
|
||||
});
|
||||
|
||||
const blankPassword = hashPassword('');
|
||||
|
||||
jsonRes(res, {
|
||||
data: data.map((item) => ({
|
||||
_id: item._id,
|
||||
shareId: item.shareId,
|
||||
name: item.name,
|
||||
password: item.password === blankPassword ? '' : '1',
|
||||
tokens: item.tokens,
|
||||
maxContext: item.maxContext,
|
||||
lastTime: item.lastTime
|
||||
|
||||
@@ -131,6 +131,7 @@ export async function chatCompletion({
|
||||
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||
const chatAPI = getOpenAIApi();
|
||||
console.log(adaptMessages);
|
||||
|
||||
/* count response max token */
|
||||
const promptsToken = modelToolMap.countTokens({
|
||||
|
||||
@@ -23,7 +23,6 @@ type FastGptWebChatProps = {
|
||||
appId?: string;
|
||||
};
|
||||
type FastGptShareChatProps = {
|
||||
password?: string;
|
||||
shareId?: string;
|
||||
};
|
||||
export type Props = CreateChatCompletionRequest &
|
||||
@@ -31,6 +30,7 @@ export type Props = CreateChatCompletionRequest &
|
||||
FastGptShareChatProps & {
|
||||
messages: MessageItemType[];
|
||||
stream?: boolean;
|
||||
variables: Record<string, any>;
|
||||
};
|
||||
export type ChatResponseType = {
|
||||
newChatId: string;
|
||||
@@ -46,7 +46,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
res.end();
|
||||
});
|
||||
|
||||
let { chatId, appId, shareId, password = '', stream = false, messages = [] } = req.body as Props;
|
||||
let { chatId, appId, shareId, stream = false, messages = [], variables = {} } = req.body as Props;
|
||||
|
||||
try {
|
||||
if (!messages) {
|
||||
@@ -66,8 +66,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
authType
|
||||
} = await (shareId
|
||||
? authShareChat({
|
||||
shareId,
|
||||
password
|
||||
shareId
|
||||
})
|
||||
: authUser({ req }));
|
||||
|
||||
@@ -105,6 +104,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
const { responseData, answerText } = await dispatchModules({
|
||||
res,
|
||||
modules: app.modules,
|
||||
variables,
|
||||
params: {
|
||||
history: prompts,
|
||||
userChatInput: prompt.value
|
||||
@@ -175,18 +175,20 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
}
|
||||
});
|
||||
|
||||
async function dispatchModules({
|
||||
export async function dispatchModules({
|
||||
res,
|
||||
modules,
|
||||
params = {},
|
||||
variables = {},
|
||||
stream = false
|
||||
}: {
|
||||
res: NextApiResponse;
|
||||
modules: AppModuleItemType[];
|
||||
params?: Record<string, any>;
|
||||
variables?: Record<string, any>;
|
||||
stream?: boolean;
|
||||
}) {
|
||||
const runningModules = loadModules(modules);
|
||||
const runningModules = loadModules(modules, variables);
|
||||
let storeData: Record<string, any> = {};
|
||||
let responseData: Record<string, any> = {};
|
||||
let answerText = '';
|
||||
@@ -333,7 +335,10 @@ async function dispatchModules({
|
||||
};
|
||||
}
|
||||
|
||||
function loadModules(modules: AppModuleItemType[]): RunningModuleItemType[] {
|
||||
function loadModules(
|
||||
modules: AppModuleItemType[],
|
||||
variables: Record<string, any>
|
||||
): RunningModuleItemType[] {
|
||||
return modules.map((module) => {
|
||||
return {
|
||||
moduleId: module.moduleId,
|
||||
@@ -341,10 +346,25 @@ function loadModules(modules: AppModuleItemType[]): RunningModuleItemType[] {
|
||||
url: module.url,
|
||||
inputs: module.inputs
|
||||
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value
|
||||
})),
|
||||
.map((item) => {
|
||||
if (typeof item.value !== 'string') {
|
||||
return {
|
||||
key: item.key,
|
||||
value: item.value
|
||||
};
|
||||
}
|
||||
|
||||
// variables replace
|
||||
const replacedVal = item.value.replace(
|
||||
/{{(.*?)}}/g,
|
||||
(match, key) => variables[key.trim()] || match
|
||||
);
|
||||
|
||||
return {
|
||||
key: item.key,
|
||||
value: replacedVal
|
||||
};
|
||||
}),
|
||||
outputs: module.outputs.map((item) => ({
|
||||
key: item.key,
|
||||
answer: item.key === SpecificInputEnum.answerText,
|
||||
|
||||
@@ -8,7 +8,7 @@ const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const API = ({ modelId }: { modelId: string }) => {
|
||||
const API = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
|
||||
@@ -33,9 +33,9 @@ const API = ({ modelId }: { modelId: string }) => {
|
||||
ml={2}
|
||||
fontWeight={'bold'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(modelId, '已复制 AppId')}
|
||||
onClick={() => copyData(appId, '已复制 AppId')}
|
||||
>
|
||||
{modelId}
|
||||
{appId}
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const Settings = ({ modelId }: { modelId: string }) => {
|
||||
const Settings = ({ appId }: { appId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
@@ -136,10 +136,10 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
);
|
||||
|
||||
// load model data
|
||||
const { isLoading } = useQuery([modelId], () => loadAppDetail(modelId, true), {
|
||||
const { isLoading } = useQuery([appId], () => loadAppDetail(appId, true), {
|
||||
onSuccess(res) {
|
||||
res && reset(res);
|
||||
modelId && setLastModelId(modelId);
|
||||
appId && setLastModelId(appId);
|
||||
setRefresh(!refresh);
|
||||
},
|
||||
onError(err: any) {
|
||||
@@ -240,7 +240,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
router.prefetch('/chat');
|
||||
await saveUpdateModel();
|
||||
} catch (error) {}
|
||||
router.push(`/chat?appId=${modelId}`);
|
||||
router.push(`/chat?appId=${appId}`);
|
||||
}}
|
||||
>
|
||||
对话
|
||||
|
||||
@@ -38,7 +38,7 @@ import { defaultShareChat } from '@/constants/model';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Share = ({ modelId }: { modelId: string }) => {
|
||||
const Share = ({ appId }: { appId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { copyData } = useCopyData();
|
||||
@@ -63,7 +63,7 @@ const Share = ({ modelId }: { modelId: string }) => {
|
||||
isFetching,
|
||||
data: shareChatList = [],
|
||||
refetch: refetchShareChatList
|
||||
} = useQuery(['initShareChatList', modelId], () => getShareChatList(modelId));
|
||||
} = useQuery(['initShareChatList', appId], () => getShareChatList(appId));
|
||||
|
||||
const onclickCreateShareChat = useCallback(
|
||||
async (e: ShareChatEditType) => {
|
||||
@@ -71,13 +71,12 @@ const Share = ({ modelId }: { modelId: string }) => {
|
||||
setIsLoading(true);
|
||||
const id = await createShareChat({
|
||||
...e,
|
||||
modelId
|
||||
appId
|
||||
});
|
||||
onCloseCreateShareChat();
|
||||
refetchShareChatList();
|
||||
|
||||
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}
|
||||
${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}`;
|
||||
copyData(url, '已复制分享地址');
|
||||
|
||||
resetShareChat(defaultShareChat);
|
||||
@@ -91,8 +90,8 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
setIsLoading(false);
|
||||
},
|
||||
[
|
||||
appId,
|
||||
copyData,
|
||||
modelId,
|
||||
onCloseCreateShareChat,
|
||||
refetchShareChatList,
|
||||
resetShareChat,
|
||||
@@ -136,7 +135,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>名称</Th>
|
||||
<Th>密码</Th>
|
||||
<Th>最大上下文</Th>
|
||||
<Th>tokens消耗</Th>
|
||||
<Th>最后使用时间</Th>
|
||||
@@ -147,7 +145,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
{shareChatList.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.name}</Td>
|
||||
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
|
||||
<Td>{item.maxContext}</Td>
|
||||
<Td>{formatTokens(item.tokens)}</Td>
|
||||
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
||||
@@ -160,7 +157,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item._id}`;
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
copyData(url, '已复制分享地址');
|
||||
}}
|
||||
/>
|
||||
@@ -216,17 +213,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'} w={0}>
|
||||
密码:
|
||||
</Box>
|
||||
<Input placeholder={'不设置密码,可直接访问'} {...registerShareChat('password')} />
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} ml={'60px'} color={'myGray.600'}>
|
||||
密码不会再次展示,请记住你的密码
|
||||
</Box>
|
||||
</FormControl>
|
||||
<FormControl mt={9}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 120px'} w={0}>
|
||||
|
||||
@@ -1,66 +1,157 @@
|
||||
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 React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
ForwardedRef
|
||||
} from 'react';
|
||||
import { Box, Flex, IconButton, useOutsideClick } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { streamFetch } from '@/api/fetch';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
|
||||
|
||||
const ChatTest = ({
|
||||
app,
|
||||
modules,
|
||||
onClose
|
||||
}: {
|
||||
app: AppSchema;
|
||||
modules?: AppModuleItemType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const isOpen = useMemo(() => !!modules, [modules]);
|
||||
export type ChatTestComponentRef = {
|
||||
resetChatTest: () => void;
|
||||
};
|
||||
|
||||
const { ChatBox, ChatInput, ChatBoxParentRef, setChatHistory } = useChat({
|
||||
appId: app._id
|
||||
const ChatTest = (
|
||||
{
|
||||
app,
|
||||
modules = [],
|
||||
onClose
|
||||
}: {
|
||||
app: AppSchema;
|
||||
modules?: AppModuleItemType[];
|
||||
onClose: () => void;
|
||||
},
|
||||
ref: ForwardedRef<ChatTestComponentRef>
|
||||
) => {
|
||||
const BoxRef = useRef(null);
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const isOpen = useMemo(() => modules && modules.length > 0, [modules]);
|
||||
|
||||
const variableModules = useMemo(
|
||||
() =>
|
||||
modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs.find((item) => item.key === SystemInputEnum.variables)?.value,
|
||||
[modules]
|
||||
);
|
||||
const welcomeText = useMemo(
|
||||
() =>
|
||||
modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs?.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
|
||||
[modules]
|
||||
);
|
||||
|
||||
const startChat = useCallback(
|
||||
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
const historyMaxLen =
|
||||
modules
|
||||
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
|
||||
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
|
||||
const history = messages.slice(-historyMaxLen - 2, -2);
|
||||
console.log(history, 'history====');
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText } = await streamFetch({
|
||||
url: '/api/chat/chatTest',
|
||||
data: {
|
||||
history,
|
||||
prompt: messages[messages.length - 2].content,
|
||||
modules,
|
||||
variables
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortSignal: controller
|
||||
});
|
||||
|
||||
return { responseText };
|
||||
},
|
||||
[modules]
|
||||
);
|
||||
|
||||
useOutsideClick({
|
||||
ref: BoxRef,
|
||||
handler: () => {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetChatTest() {
|
||||
console.log(ChatBoxRef.current, '===');
|
||||
|
||||
<Box px={5}>
|
||||
<ChatInput />
|
||||
</Box>
|
||||
</Flex>
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
/>
|
||||
<Flex
|
||||
ref={BoxRef}
|
||||
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>
|
||||
<MyTooltip label={'重置'}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'clearLight'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appAvatar={app.avatar}
|
||||
variableModules={variableModules}
|
||||
welcomeText={welcomeText}
|
||||
onStartChat={startChat}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatTest;
|
||||
export default React.memo(forwardRef(ChatTest));
|
||||
|
||||
@@ -12,13 +12,7 @@ const NodeKbSearch = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'400px'}
|
||||
logo={'/icon/logo.png'}
|
||||
name={'知识库搜索'}
|
||||
moduleId={moduleId}
|
||||
{...props}
|
||||
>
|
||||
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
useDisclosure,
|
||||
useTheme,
|
||||
Grid,
|
||||
FormControl,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Container from './modules/Container';
|
||||
import { SystemInputEnum, VariableInputEnum } from '@/constants/app';
|
||||
import type { VariableItemType } from '@/types/app';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import { Label } from './render/RenderInput';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const VariableTypeList = [
|
||||
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
|
||||
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
|
||||
];
|
||||
const defaultVariable: VariableItemType = {
|
||||
id: nanoid(),
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
type: VariableInputEnum.input,
|
||||
required: true,
|
||||
maxLen: 50,
|
||||
enums: [{ value: '' }]
|
||||
};
|
||||
|
||||
const NodeUserGuide = ({
|
||||
data: { inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const variables = useMemo(
|
||||
() =>
|
||||
(inputs.find((item) => item.key === SystemInputEnum.variables)
|
||||
?.value as VariableItemType[]) || [],
|
||||
[inputs]
|
||||
);
|
||||
const welcomeText = useMemo(
|
||||
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
const { reset, getValues, setValue, register, control, handleSubmit } = useForm<{
|
||||
variable: VariableItemType;
|
||||
}>({
|
||||
defaultValues: {
|
||||
variable: defaultVariable
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
remove: removeEnums
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'variable.enums'
|
||||
});
|
||||
|
||||
const updateVariables = useCallback(
|
||||
(value: VariableItemType[]) => {
|
||||
onChangeNode({
|
||||
moduleId: props.moduleId,
|
||||
key: SystemInputEnum.variables,
|
||||
type: 'inputs',
|
||||
value
|
||||
});
|
||||
},
|
||||
[onChangeNode, props.moduleId]
|
||||
);
|
||||
|
||||
const onclickSubmit = useCallback(
|
||||
({ variable }: { variable: VariableItemType }) => {
|
||||
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
|
||||
onClose();
|
||||
},
|
||||
[onClose, updateVariables, variables]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCard minW={'300px'} {...props}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
|
||||
<Box>对话开场白</Box>
|
||||
</Flex>
|
||||
<Textarea
|
||||
className="nodrag"
|
||||
rows={3}
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText}
|
||||
bg={'myWhite.600'}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId: props.moduleId,
|
||||
key: SystemInputEnum.welcomeText,
|
||||
type: 'inputs',
|
||||
value: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<Box mt={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'variable'} mr={2} w={'20px'} color={'#FF8A4C'} />
|
||||
<Box>变量</Box>
|
||||
<MyTooltip
|
||||
label={`变量会在开始对话前输入,仅会在本次对话中生效。\n你可以在任何字符串模块(系统提示词、限定词等)中使用 {{变量key}} 来代表变量输入。`}
|
||||
>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>变量名</Th>
|
||||
<Th>变量 key</Th>
|
||||
<Th>必填</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{variables.map((item, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{item.label} </Td>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
onOpen();
|
||||
reset({ variable: item });
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
updateVariables(variables.filter((variable) => variable.id !== item.id))
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box mt={2} textAlign={'right'}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
const newVariable = { ...defaultVariable, id: nanoid() };
|
||||
updateVariables(variables.concat(newVariable));
|
||||
reset({ variable: newVariable });
|
||||
onOpen();
|
||||
}}
|
||||
>
|
||||
+ 新增
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
<Modal isOpen={isOpen} onClose={() => {}}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'Min(400px,90vw)'}>
|
||||
<ModalHeader display={'flex'}>
|
||||
<MyIcon name={'variable'} mr={2} w={'24px'} color={'#FF8A4C'} />
|
||||
变量设置
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box w={'70px'}>必填</Box>
|
||||
<Switch {...register('variable.required')} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={'80px'}>变量名</Box>
|
||||
<Input {...register('variable.label', { required: '变量名不能为空' })} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={'80px'}>变量 key</Box>
|
||||
<Input {...register('variable.key', { required: '变量 key 不能为空' })} />
|
||||
</Flex>
|
||||
|
||||
<Box mt={5} mb={2}>
|
||||
字段类型
|
||||
</Box>
|
||||
<Grid gridTemplateColumns={'repeat(2,130px)'} gridGap={4}>
|
||||
{VariableTypeList.map((item) => (
|
||||
<Flex
|
||||
key={item.key}
|
||||
px={4}
|
||||
py={1}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
{...(item.key === getValues('variable.type')
|
||||
? {
|
||||
bg: 'myWhite.600'
|
||||
}
|
||||
: {
|
||||
_hover: {
|
||||
boxShadow: 'md'
|
||||
},
|
||||
onClick: () => {
|
||||
setValue('variable.type', item.key);
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<MyIcon name={item.icon as any} w={'16px'} />
|
||||
<Box ml={3}>{item.label}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{getValues('variable.type') === VariableInputEnum.input && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
最大长度
|
||||
</Box>
|
||||
<Box>
|
||||
<NumberInput max={100} min={1} step={1} position={'relative'}>
|
||||
<NumberInputField
|
||||
{...register('variable.maxLen', {
|
||||
min: 1,
|
||||
max: 100,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
max={100}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{getValues('variable.type') === VariableInputEnum.select && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
选项
|
||||
</Box>
|
||||
<Box>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} mb={2} alignItems={'center'}>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...register(`variable.enums.${i}.value`, {
|
||||
required: '选项内容不能为空'
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'lg'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'solid'}
|
||||
w={'100%'}
|
||||
textAlign={'left'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
bg={'myGray.100 !important'}
|
||||
onClick={() => appendEnums({ value: '' })}
|
||||
>
|
||||
添加选项
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onclickSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeUserGuide);
|
||||
@@ -14,10 +14,10 @@ const ModuleStoreList = ({
|
||||
onAddNode: (e: { template: AppModuleTemplateItemType; position: XYPosition }) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const ContextMenuRef = useRef(null);
|
||||
const BoxRef = useRef(null);
|
||||
|
||||
useOutsideClick({
|
||||
ref: ContextMenuRef,
|
||||
ref: BoxRef,
|
||||
handler: () => {
|
||||
onClose();
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const ModuleStoreList = ({
|
||||
></Box>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
ref={ContextMenuRef}
|
||||
ref={BoxRef}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
@@ -52,7 +52,7 @@ const ModuleStoreList = ({
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||
添加模块
|
||||
系统模块
|
||||
</Box>
|
||||
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||
{ModuleTemplates.map((item) =>
|
||||
|
||||
@@ -3,12 +3,15 @@ import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import type { FlowModuleItemType } from '@/types/flow';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode | React.ReactNode[] | string;
|
||||
logo?: string;
|
||||
name?: string;
|
||||
intro?: string;
|
||||
logo: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
intro: string;
|
||||
minW?: string | number;
|
||||
moduleId: string;
|
||||
onDelNode: FlowModuleItemType['onDelNode'];
|
||||
@@ -18,6 +21,7 @@ const NodeCard = ({
|
||||
children,
|
||||
logo = '/icon/logo.png',
|
||||
name = '未知模块',
|
||||
description,
|
||||
minW = '300px',
|
||||
onDelNode,
|
||||
moduleId
|
||||
@@ -28,9 +32,15 @@ const NodeCard = ({
|
||||
<Box minW={minW} bg={'white'} border={theme.borders.md} borderRadius={'md'} boxShadow={'sm'}>
|
||||
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
|
||||
<Avatar src={logo} borderRadius={'md'} w={'30px'} h={'30px'} />
|
||||
<Box ml={3} flex={1} fontSize={'lg'} color={'myGray.600'}>
|
||||
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
|
||||
{name}
|
||||
</Box>
|
||||
{description && (
|
||||
<MyTooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
<MyIcon
|
||||
className={'nodrag'}
|
||||
name="delete"
|
||||
|
||||
@@ -17,7 +17,7 @@ import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Label = ({
|
||||
export const Label = ({
|
||||
required = false,
|
||||
children,
|
||||
description
|
||||
|
||||
@@ -28,6 +28,9 @@ import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import TemplateList from './components/TemplateList';
|
||||
import ChatTest, { type ChatTestComponentRef } from './components/ChatTest';
|
||||
|
||||
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
||||
ssr: false
|
||||
});
|
||||
@@ -46,15 +49,12 @@ const NodeAnswer = dynamic(() => import('./components/NodeAnswer'), {
|
||||
const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput'), {
|
||||
ssr: false
|
||||
});
|
||||
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
||||
ssr: false
|
||||
});
|
||||
const ChatTest = dynamic(() => import('./components/ChatTest'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeUserGuide = dynamic(() => import('./components/NodeUserGuide'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
@@ -63,6 +63,7 @@ import { AppModuleItemType, AppModuleTemplateItemType } from '@/types/app';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const nodeTypes = {
|
||||
[FlowModuleTypeEnum.userGuide]: NodeUserGuide,
|
||||
[FlowModuleTypeEnum.questionInputNode]: NodeQuestionInput,
|
||||
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
||||
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||
@@ -78,6 +79,7 @@ type Props = { app: AppSchema; onBack: () => void };
|
||||
|
||||
const AppEdit = ({ app, onBack }: Props) => {
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const ChatTestRef = useRef<ChatTestComponentRef>(null);
|
||||
const theme = useTheme();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
@@ -222,7 +224,10 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
errorToast: '保存配置异常'
|
||||
errorToast: '保存配置异常',
|
||||
onSuccess() {
|
||||
ChatTestRef.current?.resetChatTest();
|
||||
}
|
||||
});
|
||||
|
||||
const initData = useCallback(
|
||||
@@ -289,8 +294,6 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
aria-label={'save'}
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
onclickSave();
|
||||
setTestModules(flow2Modules());
|
||||
}}
|
||||
/>
|
||||
@@ -366,7 +369,12 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
|
||||
<ChatTest modules={testModules} app={app} onClose={() => setTestModules(undefined)} />
|
||||
<ChatTest
|
||||
ref={ChatTestRef}
|
||||
modules={testModules}
|
||||
app={app}
|
||||
onClose={() => setTestModules(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
@@ -378,4 +386,4 @@ const Flow = (data: Props) => (
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default Flow;
|
||||
export default React.memo(Flow);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import React, { useEffect, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
@@ -155,14 +155,14 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
{currentTab === TabEnum.settings && <Settings modelId={appId} />}
|
||||
{currentTab === TabEnum.settings && <Settings appId={appId} />}
|
||||
{currentTab === TabEnum.edit && (
|
||||
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
||||
</Box>
|
||||
)}
|
||||
{currentTab === TabEnum.API && <API modelId={appId} />}
|
||||
{currentTab === TabEnum.share && <Share modelId={appId} />}
|
||||
{currentTab === TabEnum.API && <API appId={appId} />}
|
||||
{currentTab === TabEnum.share && <Share appId={appId} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</PageContainer>
|
||||
|
||||
160
client/src/pages/chat/components/ChatHistorySlider.tsx
Normal file
160
client/src/pages/chat/components/ChatHistorySlider.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
useTheme,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const ChatHistorySlider = ({
|
||||
appName,
|
||||
appAvatar,
|
||||
history,
|
||||
activeHistoryId,
|
||||
onChangeChat,
|
||||
onDelHistory,
|
||||
onCloseSlider
|
||||
}: {
|
||||
appName: string;
|
||||
appAvatar: string;
|
||||
history: {
|
||||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
activeHistoryId: string;
|
||||
onChangeChat: (historyId?: string) => void;
|
||||
onDelHistory: (historyId: string) => void;
|
||||
onCloseSlider: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
bg={'white'}
|
||||
px={[2, 5]}
|
||||
borderRight={['', theme.borders.base]}
|
||||
>
|
||||
{isPc && (
|
||||
<Flex pt={5} pb={2} alignItems={'center'} whiteSpace={'nowrap'}>
|
||||
<Avatar src={appAvatar} />
|
||||
<Box ml={2} fontWeight={'bold'} className={'textEllipsis'}>
|
||||
{appName}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{/* 新对话 */}
|
||||
<Box w={'100%'} h={'36px'} my={5}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
color={'myBlue.700'}
|
||||
borderRadius={'xl'}
|
||||
leftIcon={<MyIcon name={'edit'} w={'16px'} />}
|
||||
overflow={'hidden'}
|
||||
onClick={() => onChangeChat()}
|
||||
>
|
||||
新对话
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* chat history */}
|
||||
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
|
||||
{history.map((item) => (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
key={item.id}
|
||||
alignItems={'center'}
|
||||
py={3}
|
||||
px={4}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
borderRadius={'lg'}
|
||||
mb={2}
|
||||
_hover={{
|
||||
bg: 'myGray.100',
|
||||
'& .more': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
{...(item.id === activeHistoryId
|
||||
? {
|
||||
backgroundColor: 'myBlue.100 !important',
|
||||
color: 'myBlue.700'
|
||||
}
|
||||
: {
|
||||
onClick: () => {
|
||||
onChangeChat(item.id);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<MyIcon name={item.id === activeHistoryId ? 'chatFill' : 'chatLight'} w={'16px'} />
|
||||
<Box flex={'1 0 0'} ml={3} className="textEllipsis">
|
||||
{item.title}
|
||||
</Box>
|
||||
<Box className="more" display={['block', 'none']}>
|
||||
<Menu autoSelect={false} isLazy offset={[0, 5]}>
|
||||
<MenuButton
|
||||
_hover={{ bg: 'white' }}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'more'} w={'14px'} p={1} />
|
||||
</MenuButton>
|
||||
<MenuList color={'myGray.700'} minW={`90px !important`}>
|
||||
<MenuItem>
|
||||
<MyIcon mr={2} name={'setTop'} w={'16px'}></MyIcon>
|
||||
置顶
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelHistory(item.id);
|
||||
if (item.id === activeHistoryId) {
|
||||
onChangeChat();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon mr={2} name={'delete'} w={'16px'}></MyIcon>
|
||||
删除
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
{history.length === 0 && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
还没有聊天记录
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatHistorySlider;
|
||||
@@ -1,212 +0,0 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
useTheme,
|
||||
Menu,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
useOutsideClick
|
||||
} from '@chakra-ui/react';
|
||||
import { ChatIcon } from '@chakra-ui/icons';
|
||||
import { useRouter } from 'next/router';
|
||||
import { formatTimeToChatTime } from '@/utils/tools';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
import styles from '../index.module.scss';
|
||||
|
||||
const PcSliderBar = ({
|
||||
onclickDelHistory,
|
||||
onclickExportChat,
|
||||
onCloseSlider
|
||||
}: {
|
||||
onclickDelHistory: (historyId: string) => void;
|
||||
onclickExportChat: (type: ExportChatType) => void;
|
||||
onCloseSlider: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { shareId = '', historyId = '' } = router.query as {
|
||||
shareId: string;
|
||||
historyId: string;
|
||||
};
|
||||
const theme = useTheme();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const ContextMenuRef = useRef(null);
|
||||
const onclickContext = useRef(false);
|
||||
|
||||
const [contextMenuData, setContextMenuData] = useState<{
|
||||
left: number;
|
||||
top: number;
|
||||
history: ShareChatHistoryItemType;
|
||||
}>();
|
||||
|
||||
const { shareChatHistory } = useChatStore();
|
||||
|
||||
// close contextMenu
|
||||
useOutsideClick({
|
||||
ref: ContextMenuRef,
|
||||
handler: () => {
|
||||
setTimeout(() => {
|
||||
if (contextMenuData && !onclickContext.current) {
|
||||
setContextMenuData(undefined);
|
||||
}
|
||||
}, 10);
|
||||
setTimeout(() => {
|
||||
onclickContext.current = false;
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
const onclickContextMenu = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>, history: ShareChatHistoryItemType) => {
|
||||
e.preventDefault(); // 阻止默认右键菜单
|
||||
|
||||
if (!isPc) return;
|
||||
onclickContext.current = true;
|
||||
|
||||
setContextMenuData({
|
||||
left: e.clientX,
|
||||
top: e.clientY,
|
||||
history
|
||||
});
|
||||
},
|
||||
[isPc]
|
||||
);
|
||||
|
||||
const replaceChatPage = useCallback(
|
||||
({ hId = '', shareId }: { hId?: string; shareId: string }) => {
|
||||
if (hId === historyId) return;
|
||||
|
||||
router.replace(`/chat/share?shareId=${shareId}&historyId=${hId}`);
|
||||
!isPc && onCloseSlider();
|
||||
},
|
||||
[historyId, isPc, onCloseSlider, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
bg={'white'}
|
||||
borderRight={['', theme.borders.base]}
|
||||
>
|
||||
{/* 新对话 */}
|
||||
<Box
|
||||
className={styles.newChat}
|
||||
zIndex={1000}
|
||||
w={'90%'}
|
||||
h={'40px'}
|
||||
my={5}
|
||||
mx={'auto'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Button
|
||||
variant={'base'}
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
leftIcon={<AddIcon />}
|
||||
onClick={() => replaceChatPage({ shareId })}
|
||||
>
|
||||
新对话
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* chat history */}
|
||||
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
|
||||
{shareChatHistory.map((item) => (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
py={3}
|
||||
pr={[0, 3]}
|
||||
pl={[6, 3]}
|
||||
cursor={'pointer'}
|
||||
transition={'background-color .2s ease-in'}
|
||||
borderLeft={['none', '5px solid transparent']}
|
||||
userSelect={'none'}
|
||||
_hover={{
|
||||
backgroundColor: ['', '#dee0e3']
|
||||
}}
|
||||
{...(item._id === historyId
|
||||
? {
|
||||
backgroundColor: '#eff0f1',
|
||||
borderLeftColor: 'myBlue.600 !important'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => replaceChatPage({ hId: item._id, shareId: item.shareId })}
|
||||
onContextMenu={(e) => onclickContextMenu(e, item)}
|
||||
>
|
||||
<ChatIcon fontSize={'16px'} color={'myGray.500'} />
|
||||
<Box flex={'1 0 0'} w={0} ml={3}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" color={'myGray.1000'}>
|
||||
{item.title}
|
||||
</Box>
|
||||
<Box color={'myGray.400'} fontSize={'sm'}>
|
||||
{formatTimeToChatTime(item.updateTime)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box className="textEllipsis" mt={1} fontSize={'sm'} color={'myGray.500'}>
|
||||
{item.latestChat || '……'}
|
||||
</Box>
|
||||
</Box>
|
||||
{/* phone quick delete */}
|
||||
{!isPc && (
|
||||
<MyIcon
|
||||
px={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
onClickCapture={(e) => {
|
||||
e.stopPropagation();
|
||||
onclickDelHistory(item._id);
|
||||
item._id === historyId && replaceChatPage({ shareId: item.shareId });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
{shareChatHistory.length === 0 && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
还没有聊天记录
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
{/* context menu */}
|
||||
{contextMenuData && (
|
||||
<Box zIndex={10} position={'fixed'} top={contextMenuData.top} left={contextMenuData.left}>
|
||||
<Box ref={ContextMenuRef}></Box>
|
||||
<Menu isOpen>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onclickDelHistory(contextMenuData.history._id);
|
||||
contextMenuData.history._id === historyId && replaceChatPage({ shareId });
|
||||
}}
|
||||
>
|
||||
删除记录
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('md')}>导出Markdown格式</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default PcSliderBar;
|
||||
@@ -1,391 +1,278 @@
|
||||
import React, { useCallback, useState, useRef, useMemo, useEffect, MouseEvent } from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { initShareChatInfo } from '@/api/chat';
|
||||
import type { ChatSiteItemType, ExportChatType } from '@/types/chat';
|
||||
import {
|
||||
Textarea,
|
||||
Box,
|
||||
Flex,
|
||||
useColorModeValue,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
Drawer,
|
||||
DrawerOverlay,
|
||||
DrawerContent,
|
||||
Card,
|
||||
useOutsideClick,
|
||||
useTheme,
|
||||
Input,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
useTheme
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
|
||||
import { streamFetch } from '@/api/fetch';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { throttle } from 'lodash';
|
||||
import { Types } from 'mongoose';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { fileDownload } from '@/utils/file';
|
||||
import { htmlTemplate } from '@/constants/common';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import Loading from '@/components/Loading';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useShareChatStore, defaultHistory } from '@/store/shareChat';
|
||||
import SideBar from '@/components/SideBar';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import Empty from './components/Empty';
|
||||
import { HUMAN_ICON } from '@/constants/chat';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { gptMessage2ChatType } from '@/utils/adapt';
|
||||
import ChatHistorySlider from './components/ChatHistorySlider';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
|
||||
loading: () => <Loading fixed={false} />,
|
||||
ssr: false
|
||||
});
|
||||
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Tag from '@/components/Tag';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
|
||||
const textareaMinH = '22px';
|
||||
|
||||
const Chat = () => {
|
||||
const ShareChat = () => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { shareId = '', historyId } = router.query as { shareId: string; historyId: string };
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const ContextMenuRef = useRef(null);
|
||||
const PhoneContextShow = useRef(false);
|
||||
|
||||
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
||||
// message messageContextMenuData
|
||||
left: number;
|
||||
top: number;
|
||||
message: ChatSiteItemType;
|
||||
}>();
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
|
||||
const {
|
||||
password,
|
||||
setPassword,
|
||||
shareChatHistory,
|
||||
delShareHistoryById,
|
||||
setShareChatHistory,
|
||||
shareChatData,
|
||||
setShareChatData,
|
||||
delShareChatHistoryItemById,
|
||||
delShareChatHistory
|
||||
} = useChatStore();
|
||||
|
||||
const isChatting = useMemo(
|
||||
() => shareChatData.history[shareChatData.history.length - 1]?.status === 'loading',
|
||||
[shareChatData.history]
|
||||
);
|
||||
|
||||
const { ChatBox, ChatInput, ChatBoxParentRef, setChatHistory, scrollToBottom } = useChat({
|
||||
appId: shareChatData.appId
|
||||
});
|
||||
|
||||
const { toast } = useToast();
|
||||
const { copyData } = useCopyData();
|
||||
const { isPc } = useGlobalStore();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenPassword,
|
||||
onClose: onClosePassword,
|
||||
onOpen: onOpenPassword
|
||||
} = useDisclosure();
|
||||
|
||||
// close contextMenu
|
||||
useOutsideClick({
|
||||
ref: ContextMenuRef,
|
||||
handler: () => {
|
||||
// 移动端长按后会将其设置为true,松手时候也会触发一次,松手的时候需要忽略一次。
|
||||
if (PhoneContextShow.current) {
|
||||
PhoneContextShow.current = false;
|
||||
} else {
|
||||
messageContextMenuData &&
|
||||
setTimeout(() => {
|
||||
setMessageContextMenuData(undefined);
|
||||
window.getSelection?.()?.empty?.();
|
||||
window.getSelection?.()?.removeAllRanges?.();
|
||||
document?.getSelection()?.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// export chat data
|
||||
const onclickExportChat = useCallback(
|
||||
(type: ExportChatType) => {
|
||||
const getHistoryHtml = () => {
|
||||
const historyDom = document.getElementById('history');
|
||||
if (!historyDom) return;
|
||||
const dom = Array.from(historyDom.children).map((child, i) => {
|
||||
const avatar = `<img src="${
|
||||
child.querySelector<HTMLImageElement>('.avatar')?.src
|
||||
}" alt="" />`;
|
||||
|
||||
const chatContent = child.querySelector<HTMLDivElement>('.markdown');
|
||||
|
||||
if (!chatContent) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const chatContentClone = chatContent.cloneNode(true) as HTMLDivElement;
|
||||
|
||||
const codeHeader = chatContentClone.querySelectorAll('.code-header');
|
||||
codeHeader.forEach((childElement: any) => {
|
||||
childElement.remove();
|
||||
});
|
||||
|
||||
return `<div class="chat-item">
|
||||
${avatar}
|
||||
${chatContentClone.outerHTML}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
const html = htmlTemplate.replace('{{CHAT_CONTENT}}', dom.join('\n'));
|
||||
return html;
|
||||
};
|
||||
|
||||
const map: Record<ExportChatType, () => void> = {
|
||||
md: () => {
|
||||
fileDownload({
|
||||
text: shareChatData.history.map((item) => item.value).join('\n\n'),
|
||||
type: 'text/markdown',
|
||||
filename: 'chat.md'
|
||||
});
|
||||
},
|
||||
html: () => {
|
||||
const html = getHistoryHtml();
|
||||
html &&
|
||||
fileDownload({
|
||||
text: html,
|
||||
type: 'text/html',
|
||||
filename: '聊天记录.html'
|
||||
});
|
||||
},
|
||||
pdf: () => {
|
||||
const html = getHistoryHtml();
|
||||
|
||||
html &&
|
||||
// @ts-ignore
|
||||
html2pdf(html, {
|
||||
margin: 0,
|
||||
filename: `聊天记录.pdf`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
map[type]();
|
||||
},
|
||||
[shareChatData.history]
|
||||
);
|
||||
|
||||
// 获取对话信息
|
||||
const loadChatInfo = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await initShareChatInfo({
|
||||
shareId,
|
||||
password
|
||||
});
|
||||
|
||||
const history = shareChatHistory.find((item) => item._id === historyId)?.chats || [];
|
||||
|
||||
setShareChatData({
|
||||
...res,
|
||||
history
|
||||
});
|
||||
|
||||
onClosePassword();
|
||||
|
||||
history.length > 0 &&
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 500);
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: typeof e === 'string' ? e : e?.message || '初始化异常'
|
||||
});
|
||||
if (e?.code === 501) {
|
||||
onOpenPassword();
|
||||
} else {
|
||||
delShareChatHistory(shareId);
|
||||
router.replace(`/chat/share`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
return null;
|
||||
}, [
|
||||
setIsLoading,
|
||||
shareId,
|
||||
password,
|
||||
setShareChatData,
|
||||
shareChatHistory,
|
||||
onClosePassword,
|
||||
historyId,
|
||||
scrollToBottom,
|
||||
toast,
|
||||
onOpenPassword,
|
||||
delShareChatHistory,
|
||||
router
|
||||
]);
|
||||
saveChatResponse,
|
||||
delShareChatHistoryItemById,
|
||||
delOneShareHistoryByHistoryId,
|
||||
delManyShareChatHistoryByShareId
|
||||
} = useShareChatStore();
|
||||
|
||||
const startChat = useCallback(
|
||||
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
console.log(messages, variables);
|
||||
|
||||
const prompts = messages.slice(-shareChatData.maxContext - 2);
|
||||
const { responseText } = await streamFetch({
|
||||
data: {
|
||||
history,
|
||||
messages: prompts,
|
||||
variables,
|
||||
shareId
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortSignal: controller
|
||||
});
|
||||
|
||||
const result = {
|
||||
question: messages[messages.length - 2].content || '',
|
||||
answer: responseText
|
||||
};
|
||||
|
||||
prompts[prompts.length - 1].content = responseText;
|
||||
|
||||
/* save chat */
|
||||
const { newChatId } = saveChatResponse({
|
||||
historyId,
|
||||
prompts: gptMessage2ChatType(prompts).map((item) => ({
|
||||
...item,
|
||||
status: 'finish'
|
||||
})),
|
||||
variables,
|
||||
shareId
|
||||
});
|
||||
|
||||
if (newChatId) {
|
||||
router.replace({
|
||||
query: {
|
||||
shareId,
|
||||
historyId: newChatId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.top?.postMessage(
|
||||
{
|
||||
type: 'shareChatFinish',
|
||||
data: result
|
||||
},
|
||||
'*'
|
||||
);
|
||||
|
||||
return { responseText };
|
||||
},
|
||||
[historyId, router, saveChatResponse, shareChatData.maxContext, shareId]
|
||||
);
|
||||
|
||||
const loadAppInfo = useCallback(
|
||||
async (shareId?: string) => {
|
||||
if (!shareId) return null;
|
||||
const history = shareChatHistory.find((item) => item._id === historyId) || defaultHistory;
|
||||
|
||||
ChatBoxRef.current?.resetHistory(history.chats);
|
||||
ChatBoxRef.current?.resetVariables(history.variables);
|
||||
|
||||
try {
|
||||
const chatData = await (async () => {
|
||||
if (shareChatData.app.name === '') {
|
||||
return initShareChatInfo({
|
||||
shareId
|
||||
});
|
||||
}
|
||||
return shareChatData;
|
||||
})();
|
||||
|
||||
setShareChatData({
|
||||
...chatData,
|
||||
history
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(e, '获取应用失败')
|
||||
});
|
||||
if (e?.code === 501) {
|
||||
delManyShareChatHistoryByShareId(shareId);
|
||||
}
|
||||
}
|
||||
|
||||
return history;
|
||||
},
|
||||
[
|
||||
delManyShareChatHistoryByShareId,
|
||||
historyId,
|
||||
setShareChatData,
|
||||
shareChatData,
|
||||
shareChatHistory,
|
||||
toast
|
||||
]
|
||||
);
|
||||
|
||||
// 初始化聊天框
|
||||
useQuery(['init', shareId, historyId], () => {
|
||||
if (!shareId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!historyId) {
|
||||
return router.replace(`/chat/share?shareId=${shareId}&historyId=${new Types.ObjectId()}`);
|
||||
}
|
||||
|
||||
return loadChatInfo();
|
||||
return loadAppInfo(shareId);
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}>
|
||||
{/* pc always show history. */}
|
||||
{isPc && (
|
||||
<SideBar>
|
||||
<ShareHistory
|
||||
onclickDelHistory={delShareHistoryById}
|
||||
onclickExportChat={onclickExportChat}
|
||||
onCloseSlider={onCloseSlider}
|
||||
/>
|
||||
</SideBar>
|
||||
)}
|
||||
|
||||
{/* 聊天内容 */}
|
||||
<Flex
|
||||
position={'relative'}
|
||||
h={[0, '100%']}
|
||||
w={['100%', 0]}
|
||||
flex={'1 0 0'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
{/* chat header */}
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
py={[3, 5]}
|
||||
px={5}
|
||||
borderBottom={'1px solid '}
|
||||
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
color={useColorModeValue('myGray.900', 'white')}
|
||||
>
|
||||
{!isPc && (
|
||||
<MyIcon
|
||||
name={'menu'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
onClick={onOpenSlider}
|
||||
/>
|
||||
)}
|
||||
<Box lineHeight={1.2} textAlign={'center'} px={3} fontSize={['sm', 'md']}>
|
||||
{shareChatData.model.name}
|
||||
{shareChatData.history.length > 0 ? ` (${shareChatData.history.length})` : ''}
|
||||
</Box>
|
||||
{shareChatData.history.length > 0 ? (
|
||||
<Menu autoSelect={false}>
|
||||
<MenuButton lineHeight={1}>
|
||||
<MyIcon
|
||||
name={'more'}
|
||||
w={'16px'}
|
||||
h={'16px'}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList minW={`90px !important`}>
|
||||
<MenuItem onClick={() => router.replace(`/chat/share?shareId=${shareId}`)}>
|
||||
新对话
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
delShareHistoryById(historyId);
|
||||
router.replace(`/chat/share?shareId=${shareId}`);
|
||||
}}
|
||||
>
|
||||
删除记录
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('md')}>导出Markdown格式</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
) : (
|
||||
<Box w={'16px'} h={'16px'} />
|
||||
)}
|
||||
</Flex>
|
||||
{/* chat content box */}
|
||||
<Box ref={ChatBoxParentRef} flex={1}>
|
||||
<ChatBox appAvatar={shareChatData.model.avatar} />
|
||||
</Box>
|
||||
{/* 发送区 */}
|
||||
<ChatInput />
|
||||
|
||||
<Loading fixed={false} />
|
||||
</Flex>
|
||||
|
||||
{/* phone slider */}
|
||||
{!isPc && (
|
||||
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
|
||||
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
|
||||
<DrawerContent maxWidth={'250px'}>
|
||||
<ShareHistory
|
||||
onclickDelHistory={delShareHistoryById}
|
||||
onclickExportChat={onclickExportChat}
|
||||
<PageContainer>
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}>
|
||||
{/* slider */}
|
||||
{isPc ? (
|
||||
<SideBar>
|
||||
<ChatHistorySlider
|
||||
appName={shareChatData.app.name}
|
||||
appAvatar={shareChatData.app.avatar}
|
||||
activeHistoryId={historyId}
|
||||
history={shareChatHistory
|
||||
.filter((item) => item.shareId === shareId)
|
||||
.map((item) => ({
|
||||
id: item._id,
|
||||
title: item.title
|
||||
}))}
|
||||
onChangeChat={(historyId) => {
|
||||
router.push({
|
||||
query: {
|
||||
historyId: historyId || '',
|
||||
shareId
|
||||
}
|
||||
});
|
||||
}}
|
||||
onDelHistory={delOneShareHistoryByHistoryId}
|
||||
onCloseSlider={onCloseSlider}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
{/* password input */}
|
||||
{
|
||||
<Modal isOpen={isOpenPassword} onClose={onClosePassword}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalHeader>安全密码</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>密码:</Box>
|
||||
<Input
|
||||
type="password"
|
||||
autoFocus
|
||||
placeholder="使用密码,无密码直接点确认"
|
||||
onBlur={(e) => setPassword(e.target.value)}
|
||||
</SideBar>
|
||||
) : (
|
||||
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
|
||||
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
|
||||
<DrawerContent maxWidth={'250px'}>
|
||||
<ChatHistorySlider
|
||||
appName={shareChatData.app.name}
|
||||
appAvatar={shareChatData.app.avatar}
|
||||
activeHistoryId={historyId}
|
||||
history={shareChatHistory.map((item) => ({
|
||||
id: item._id,
|
||||
title: item.title
|
||||
}))}
|
||||
onChangeChat={(historyId) => {
|
||||
router.push({
|
||||
query: {
|
||||
historyId: historyId || '',
|
||||
shareId
|
||||
}
|
||||
});
|
||||
}}
|
||||
onDelHistory={delOneShareHistoryByHistoryId}
|
||||
onCloseSlider={onCloseSlider}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
{/* chat container */}
|
||||
<Flex
|
||||
position={'relative'}
|
||||
h={[0, '100%']}
|
||||
w={['100%', 0]}
|
||||
flex={'1 0 0'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={[3, 5]}
|
||||
px={5}
|
||||
borderBottom={theme.borders.base}
|
||||
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
color={useColorModeValue('myGray.900', 'white')}
|
||||
>
|
||||
{isPc ? (
|
||||
<>
|
||||
<Box mr={3} color={'myGray.1000'}>
|
||||
{shareChatData.history.title}
|
||||
</Box>
|
||||
<Tag display={'flex'}>
|
||||
<MyIcon name={'history'} w={'14px'} />
|
||||
<Box ml={1}>{shareChatData.history.chats.length}条记录</Box>
|
||||
</Tag>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MyIcon
|
||||
name={'menu'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
color={useColorModeValue('blackAlpha.700', 'white')}
|
||||
onClick={onOpenSlider}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClosePassword}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={loadChatInfo}>确定</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{/* chat box */}
|
||||
<Box
|
||||
pt={[0, 5]}
|
||||
flex={1}
|
||||
maxW={['100%', '1000px', '1200px']}
|
||||
px={[0, 5]}
|
||||
w={'100%'}
|
||||
mx={'auto'}
|
||||
>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appAvatar={shareChatData.app.avatar}
|
||||
variableModules={shareChatData.app.variableModules}
|
||||
welcomeText={shareChatData.app.welcomeText}
|
||||
onUpdateVariable={(e) => {
|
||||
setShareChatData((state) => ({
|
||||
...state,
|
||||
history: {
|
||||
...state.history,
|
||||
variables: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={({ index }) => delShareChatHistoryItemById({ historyId, index })}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
export default ShareChat;
|
||||
|
||||
Reference in New Issue
Block a user