This commit is contained in:
archer
2023-07-11 15:57:01 +08:00
parent cd77d81135
commit eb768d9c04
47 changed files with 1949 additions and 1280 deletions

View 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();
}
}

View File

@@ -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, {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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({

View File

@@ -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,

View File

@@ -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

View File

@@ -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}`);
}}
>

View File

@@ -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}>

View File

@@ -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));

View File

@@ -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

View File

@@ -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);

View File

@@ -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) =>

View File

@@ -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"

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View 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;

View File

@@ -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;

View File

@@ -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;