chat box
BIN
client/public/imgs/module/userGuide.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
@@ -58,15 +58,15 @@ export const putChatHistory = (data: UpdateHistoryProps) =>
|
|||||||
*/
|
*/
|
||||||
export const createShareChat = (
|
export const createShareChat = (
|
||||||
data: ShareChatEditType & {
|
data: ShareChatEditType & {
|
||||||
modelId: string;
|
appId: string;
|
||||||
}
|
}
|
||||||
) => POST<string>(`/chat/shareChat/create`, data);
|
) => POST<string>(`/chat/shareChat/create`, data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get shareChat
|
* get shareChat
|
||||||
*/
|
*/
|
||||||
export const getShareChatList = (modelId: string) =>
|
export const getShareChatList = (appId: string) =>
|
||||||
GET<ShareChatSchema[]>(`/chat/shareChat/list?modelId=${modelId}`);
|
GET<ShareChatSchema[]>(`/chat/shareChat/list`, { appId });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* delete a shareChat
|
* delete a shareChat
|
||||||
@@ -76,5 +76,5 @@ export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?i
|
|||||||
/**
|
/**
|
||||||
* 初始化分享聊天
|
* 初始化分享聊天
|
||||||
*/
|
*/
|
||||||
export const initShareChatInfo = (data: { shareId: string; password: string }) =>
|
export const initShareChatInfo = (data: { shareId: string }) =>
|
||||||
GET<InitShareChatResponse>(`/chat/shareChat/init?${Obj2Query(data)}`);
|
GET<InitShareChatResponse>(`/chat/shareChat/init`, data);
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { Props } from '@/pages/api/openapi/v1/chat/completions';
|
|
||||||
import { sseResponseEventEnum } from '@/constants/chat';
|
import { sseResponseEventEnum } from '@/constants/chat';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
import { parseStreamChunk } from '@/utils/adapt';
|
import { parseStreamChunk } from '@/utils/adapt';
|
||||||
|
|
||||||
interface StreamFetchProps {
|
interface StreamFetchProps {
|
||||||
data: Props;
|
url?: string;
|
||||||
|
data: Record<string, any>;
|
||||||
onMessage: (text: string) => void;
|
onMessage: (text: string) => void;
|
||||||
abortSignal: AbortController;
|
abortSignal: AbortController;
|
||||||
}
|
}
|
||||||
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
|
export const streamFetch = ({
|
||||||
|
url = '/api/openapi/v1/chat/completions2',
|
||||||
|
data,
|
||||||
|
onMessage,
|
||||||
|
abortSignal
|
||||||
|
}: StreamFetchProps) =>
|
||||||
new Promise<{ responseText: string; errMsg: string; newChatId: string | null }>(
|
new Promise<{ responseText: string; errMsg: string; newChatId: string | null }>(
|
||||||
async (resolve, reject) => {
|
async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const response = await window.fetch('/api/openapi/v1/chat/completions2', {
|
const response = await window.fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
9
client/src/api/response/chat.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import type { ChatPopulate, AppSchema } from '@/types/mongoSchema';
|
import type { ChatPopulate, AppSchema } from '@/types/mongoSchema';
|
||||||
import type { ChatItemType } from '@/types/chat';
|
import type { ChatItemType } from '@/types/chat';
|
||||||
|
import { VariableItemType } from '@/types/app';
|
||||||
|
|
||||||
export interface InitChatResponse {
|
export interface InitChatResponse {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@@ -17,13 +18,13 @@ export interface InitChatResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InitShareChatResponse {
|
export interface InitShareChatResponse {
|
||||||
maxContext: number;
|
|
||||||
userAvatar: string;
|
userAvatar: string;
|
||||||
appId: string;
|
maxContext: number;
|
||||||
model: {
|
app: {
|
||||||
|
variableModules?: VariableItemType[];
|
||||||
|
welcomeText?: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
intro: string;
|
intro: string;
|
||||||
};
|
};
|
||||||
chatModel: AppSchema['chat']['chatModel']; // 对话模型名
|
|
||||||
}
|
}
|
||||||
|
|||||||
601
client/src/components/ChatBox/index.tsx
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
ForwardedRef
|
||||||
|
} from 'react';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import { ChatSiteItemType } from '@/types/chat';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
|
||||||
|
import { Box, Card, Flex, Input, Textarea, Button, useTheme } from '@chakra-ui/react';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
|
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { HUMAN_ICON } from '@/constants/chat';
|
||||||
|
import Markdown from '@/components/Markdown';
|
||||||
|
import MyIcon from '@/components/Icon';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
|
||||||
|
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||||
|
import { VariableItemType } from '@/types/app';
|
||||||
|
import { VariableInputEnum } from '@/constants/app';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import MySelect from '@/components/Select';
|
||||||
|
import { MessageItemType } from '@/pages/api/openapi/v1/chat/completions';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import MyTooltip from '../MyTooltip';
|
||||||
|
|
||||||
|
const textareaMinH = '22px';
|
||||||
|
export type StartChatFnProps = {
|
||||||
|
messages: MessageItemType[];
|
||||||
|
controller: AbortController;
|
||||||
|
variables: Record<string, any>;
|
||||||
|
generatingMessage: (text: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComponentRef = {
|
||||||
|
resetVariables: (data?: Record<string, any>) => void;
|
||||||
|
resetHistory: (history: ChatSiteItemType[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VariableLabel = ({
|
||||||
|
required = false,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
required?: boolean;
|
||||||
|
children: React.ReactNode | string;
|
||||||
|
}) => (
|
||||||
|
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
|
||||||
|
{children}
|
||||||
|
{required && (
|
||||||
|
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
|
||||||
|
*
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ChatBox = (
|
||||||
|
{
|
||||||
|
appAvatar,
|
||||||
|
variableModules,
|
||||||
|
welcomeText,
|
||||||
|
onUpdateVariable,
|
||||||
|
onStartChat,
|
||||||
|
onDelMessage
|
||||||
|
}: {
|
||||||
|
appAvatar: string;
|
||||||
|
variableModules?: VariableItemType[];
|
||||||
|
welcomeText?: string;
|
||||||
|
onUpdateVariable?: (e: Record<string, any>) => void;
|
||||||
|
onStartChat: (e: StartChatFnProps) => Promise<{ responseText: string }>;
|
||||||
|
onDelMessage?: (e: { id?: string; index: number }) => void;
|
||||||
|
},
|
||||||
|
ref: ForwardedRef<ComponentRef>
|
||||||
|
) => {
|
||||||
|
const ChatBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const { copyData } = useCopyData();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { userInfo } = useUserStore();
|
||||||
|
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const controller = useRef(new AbortController());
|
||||||
|
|
||||||
|
const [variables, setVariables] = useState<Record<string, any>>({});
|
||||||
|
const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]);
|
||||||
|
|
||||||
|
const isChatting = useMemo(
|
||||||
|
() => chatHistory[chatHistory.length - 1]?.status === 'loading',
|
||||||
|
[chatHistory]
|
||||||
|
);
|
||||||
|
const variableIsFinish = useMemo(() => {
|
||||||
|
if (!variableModules || chatHistory.length > 0) return true;
|
||||||
|
|
||||||
|
for (let i = 0; i < variableModules.length; i++) {
|
||||||
|
const item = variableModules[i];
|
||||||
|
if (item.required && !variables[item.key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [chatHistory.length, variableModules, variables]);
|
||||||
|
|
||||||
|
const isLargeWidth = ChatBoxRef?.current?.clientWidth && ChatBoxRef?.current?.clientWidth >= 900;
|
||||||
|
|
||||||
|
const { register, reset, setValue, handleSubmit } = useForm<Record<string, any>>({
|
||||||
|
defaultValues: variables
|
||||||
|
});
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = useCallback(
|
||||||
|
(behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||||
|
if (!ChatBoxRef.current) return;
|
||||||
|
ChatBoxRef.current.scrollTo({
|
||||||
|
top: ChatBoxRef.current.scrollHeight,
|
||||||
|
behavior
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[ChatBoxRef]
|
||||||
|
);
|
||||||
|
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
||||||
|
const generatingScroll = useCallback(
|
||||||
|
throttle(() => {
|
||||||
|
if (!ChatBoxRef.current) return;
|
||||||
|
const isBottom =
|
||||||
|
ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >=
|
||||||
|
ChatBoxRef.current.scrollHeight;
|
||||||
|
|
||||||
|
isBottom && scrollToBottom('auto');
|
||||||
|
}, 100),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const generatingMessage = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
setChatHistory((state) =>
|
||||||
|
state.map((item, index) => {
|
||||||
|
if (index !== state.length - 1) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: item.value + text
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
generatingScroll();
|
||||||
|
},
|
||||||
|
[generatingScroll, setChatHistory]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 复制内容
|
||||||
|
const onclickCopy = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const val = value.replace(/\n+/g, '\n');
|
||||||
|
copyData(val);
|
||||||
|
},
|
||||||
|
[copyData]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 重置输入内容
|
||||||
|
const resetInputVal = useCallback((val: string) => {
|
||||||
|
if (!TextareaDom.current) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
/* 回到最小高度 */
|
||||||
|
if (TextareaDom.current) {
|
||||||
|
TextareaDom.current.value = val;
|
||||||
|
TextareaDom.current.style.height =
|
||||||
|
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* user confirm send prompt
|
||||||
|
*/
|
||||||
|
const sendPrompt = useCallback(
|
||||||
|
async (data: Record<string, any> = {}) => {
|
||||||
|
if (isChatting) {
|
||||||
|
toast({
|
||||||
|
title: '正在聊天中...请等待结束',
|
||||||
|
status: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// get input value
|
||||||
|
const value = TextareaDom.current?.value || '';
|
||||||
|
const val = value.trim().replace(/\n\s*/g, '\n');
|
||||||
|
|
||||||
|
if (!val) {
|
||||||
|
toast({
|
||||||
|
title: '内容为空',
|
||||||
|
status: 'warning'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChatList: ChatSiteItemType[] = [
|
||||||
|
...chatHistory,
|
||||||
|
{
|
||||||
|
_id: String(new Types.ObjectId()),
|
||||||
|
obj: 'Human',
|
||||||
|
value: val,
|
||||||
|
status: 'finish'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: String(new Types.ObjectId()),
|
||||||
|
obj: 'AI',
|
||||||
|
value: '',
|
||||||
|
status: 'loading'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 插入内容
|
||||||
|
setChatHistory(newChatList);
|
||||||
|
|
||||||
|
// 清空输入内容
|
||||||
|
resetInputVal('');
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// create abort obj
|
||||||
|
const abortSignal = new AbortController();
|
||||||
|
controller.current = abortSignal;
|
||||||
|
|
||||||
|
const messages = adaptChatItem_openAI({ messages: newChatList, reserveId: true });
|
||||||
|
|
||||||
|
await onStartChat({
|
||||||
|
messages,
|
||||||
|
controller: abortSignal,
|
||||||
|
generatingMessage,
|
||||||
|
variables: data
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置聊天内容为完成状态
|
||||||
|
setChatHistory((state) =>
|
||||||
|
state.map((item, index) => {
|
||||||
|
if (index !== state.length - 1) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
status: 'finish'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
generatingScroll();
|
||||||
|
TextareaDom.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast({
|
||||||
|
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||||
|
status: 'warning',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
resetInputVal(value);
|
||||||
|
|
||||||
|
setChatHistory(newChatList.slice(0, newChatList.length - 2));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isChatting,
|
||||||
|
chatHistory,
|
||||||
|
setChatHistory,
|
||||||
|
resetInputVal,
|
||||||
|
toast,
|
||||||
|
scrollToBottom,
|
||||||
|
onStartChat,
|
||||||
|
generatingMessage,
|
||||||
|
generatingScroll
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
resetVariables(e) {
|
||||||
|
const defaultVal: Record<string, any> = {};
|
||||||
|
variableModules?.forEach((item) => {
|
||||||
|
defaultVal[item.key] = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
reset(e || defaultVal);
|
||||||
|
setVariables(e || defaultVal);
|
||||||
|
},
|
||||||
|
resetHistory(e) {
|
||||||
|
setChatHistory(e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const controlIconStyle = {
|
||||||
|
w: '14px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
p: 1,
|
||||||
|
bg: 'white',
|
||||||
|
borderRadius: 'lg',
|
||||||
|
boxShadow: '0 0 5px rgba(0,0,0,0.1)',
|
||||||
|
border: theme.borders.base,
|
||||||
|
mr: 3
|
||||||
|
};
|
||||||
|
const controlContainerStyle = {
|
||||||
|
className: 'control',
|
||||||
|
display: ['flex', 'none'],
|
||||||
|
color: 'myGray.400',
|
||||||
|
pl: 1,
|
||||||
|
mt: 2,
|
||||||
|
position: 'absolute' as any,
|
||||||
|
zIndex: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDirection={'column'} h={'100%'}>
|
||||||
|
<Box ref={ChatBoxRef} flex={'1 0 0'} overflow={'overlay'} px={[2, 5]}>
|
||||||
|
{/* variable input */}
|
||||||
|
{(variableModules || welcomeText) && (
|
||||||
|
<Flex alignItems={'flex-start'} py={2}>
|
||||||
|
{/* avatar */}
|
||||||
|
<Avatar
|
||||||
|
src={appAvatar}
|
||||||
|
w={isLargeWidth ? '34px' : '24px'}
|
||||||
|
h={isLargeWidth ? '34px' : '24px'}
|
||||||
|
order={1}
|
||||||
|
mr={['6px', 2]}
|
||||||
|
/>
|
||||||
|
{/* message */}
|
||||||
|
<Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||||
|
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||||
|
{welcomeText && (
|
||||||
|
<Box mb={2} pb={2} borderBottom={theme.borders.base}>
|
||||||
|
{welcomeText}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{variableModules && (
|
||||||
|
<Box>
|
||||||
|
{variableModules.map((item) => (
|
||||||
|
<Box w={'min(100%,300px)'} key={item.id} mb={4}>
|
||||||
|
<VariableLabel required={item.required}>{item.label}</VariableLabel>
|
||||||
|
{item.type === VariableInputEnum.input && (
|
||||||
|
<Input
|
||||||
|
{...register(item.key, {
|
||||||
|
required: item.required
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === VariableInputEnum.select && (
|
||||||
|
<MySelect
|
||||||
|
width={'100%'}
|
||||||
|
list={(item.enums || []).map((item) => ({
|
||||||
|
label: item.value,
|
||||||
|
value: item.value
|
||||||
|
}))}
|
||||||
|
{...register(item.key, {
|
||||||
|
required: item.required
|
||||||
|
})}
|
||||||
|
onchange={(e) => {
|
||||||
|
setValue(item.key, e);
|
||||||
|
// setRefresh((state) => !state);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{!variableIsFinish && (
|
||||||
|
<Button
|
||||||
|
leftIcon={<MyIcon name={'chatFill'} w={'16px'} />}
|
||||||
|
size={'sm'}
|
||||||
|
maxW={'100px'}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
onClick={handleSubmit((data) => {
|
||||||
|
onUpdateVariable?.(data);
|
||||||
|
setVariables(data);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{'开始对话'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{/* chat history */}
|
||||||
|
<Box id={'history'}>
|
||||||
|
{chatHistory.map((item, index) => (
|
||||||
|
<Flex
|
||||||
|
key={item._id}
|
||||||
|
alignItems={'flex-start'}
|
||||||
|
py={2}
|
||||||
|
_hover={{
|
||||||
|
'& .control': {
|
||||||
|
display: 'flex'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.obj === 'Human' && <Box flex={1} />}
|
||||||
|
{/* avatar */}
|
||||||
|
<Avatar
|
||||||
|
src={item.obj === 'Human' ? userInfo?.avatar || HUMAN_ICON : appAvatar}
|
||||||
|
w={isLargeWidth ? '34px' : '24px'}
|
||||||
|
h={isLargeWidth ? '34px' : '24px'}
|
||||||
|
{...(item.obj === 'AI'
|
||||||
|
? {
|
||||||
|
order: 1,
|
||||||
|
mr: ['6px', 2]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
order: 3,
|
||||||
|
ml: ['6px', 2]
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/* message */}
|
||||||
|
<Box order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||||
|
{item.obj === 'AI' ? (
|
||||||
|
<Box w={'100%'}>
|
||||||
|
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||||
|
<Markdown
|
||||||
|
source={item.value}
|
||||||
|
isChatting={index === chatHistory.length - 1 && isChatting}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Flex {...controlContainerStyle}>
|
||||||
|
<MyTooltip label={'复制'}>
|
||||||
|
<MyIcon
|
||||||
|
{...controlIconStyle}
|
||||||
|
name={'copy'}
|
||||||
|
_hover={{ color: 'myBlue.700' }}
|
||||||
|
onClick={() => onclickCopy(item.value)}
|
||||||
|
/>
|
||||||
|
</MyTooltip>
|
||||||
|
{onDelMessage && (
|
||||||
|
<MyTooltip label={'删除'}>
|
||||||
|
<MyIcon
|
||||||
|
{...controlIconStyle}
|
||||||
|
name={'delete'}
|
||||||
|
_hover={{ color: 'red.600' }}
|
||||||
|
onClick={() => {
|
||||||
|
setChatHistory((state) =>
|
||||||
|
state.filter((chat) => chat._id !== item._id)
|
||||||
|
);
|
||||||
|
onDelMessage({
|
||||||
|
id: item._id,
|
||||||
|
index
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MyTooltip>
|
||||||
|
)}
|
||||||
|
{hasVoiceApi && (
|
||||||
|
<MyTooltip label={'语音播报'}>
|
||||||
|
<MyIcon
|
||||||
|
{...controlIconStyle}
|
||||||
|
name={'voice'}
|
||||||
|
_hover={{ color: '#E74694' }}
|
||||||
|
onClick={() => voiceBroadcast({ text: item.value })}
|
||||||
|
/>
|
||||||
|
</MyTooltip>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box position={'relative'}>
|
||||||
|
<Card
|
||||||
|
className="markdown"
|
||||||
|
whiteSpace={'pre-wrap'}
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius={'8px 0 8px 8px'}
|
||||||
|
bg={'myBlue.300'}
|
||||||
|
>
|
||||||
|
<Box as={'p'}>{item.value}</Box>
|
||||||
|
</Card>
|
||||||
|
<Flex {...controlContainerStyle} right={0}>
|
||||||
|
<MyTooltip label={'复制'}>
|
||||||
|
<MyIcon
|
||||||
|
{...controlIconStyle}
|
||||||
|
name={'copy'}
|
||||||
|
_hover={{ color: 'myBlue.700' }}
|
||||||
|
onClick={() => onclickCopy(item.value)}
|
||||||
|
/>
|
||||||
|
</MyTooltip>
|
||||||
|
{onDelMessage && (
|
||||||
|
<MyTooltip label={'删除'}>
|
||||||
|
<MyIcon
|
||||||
|
{...controlIconStyle}
|
||||||
|
mr={0}
|
||||||
|
name={'delete'}
|
||||||
|
_hover={{ color: 'red.600' }}
|
||||||
|
onClick={() => {
|
||||||
|
setChatHistory((state) =>
|
||||||
|
state.filter((chat) => chat._id !== item._id)
|
||||||
|
);
|
||||||
|
onDelMessage({
|
||||||
|
id: item._id,
|
||||||
|
index
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MyTooltip>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{variableIsFinish ? (
|
||||||
|
<Box m={['0 auto', '20px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']} px={[0, 5]}>
|
||||||
|
<Box
|
||||||
|
py={'18px'}
|
||||||
|
position={'relative'}
|
||||||
|
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
|
||||||
|
borderTop={['1px solid', 0]}
|
||||||
|
borderTopColor={'gray.200'}
|
||||||
|
borderRadius={['none', 'md']}
|
||||||
|
backgroundColor={'white'}
|
||||||
|
>
|
||||||
|
{/* 输入框 */}
|
||||||
|
<Textarea
|
||||||
|
ref={TextareaDom}
|
||||||
|
py={0}
|
||||||
|
pr={['45px', '55px']}
|
||||||
|
border={'none'}
|
||||||
|
_focusVisible={{
|
||||||
|
border: 'none'
|
||||||
|
}}
|
||||||
|
isDisabled={isChatting}
|
||||||
|
placeholder="提问"
|
||||||
|
resize={'none'}
|
||||||
|
rows={1}
|
||||||
|
height={'22px'}
|
||||||
|
lineHeight={'22px'}
|
||||||
|
maxHeight={'150px'}
|
||||||
|
maxLength={-1}
|
||||||
|
overflowY={'auto'}
|
||||||
|
whiteSpace={'pre-wrap'}
|
||||||
|
wordBreak={'break-all'}
|
||||||
|
boxShadow={'none !important'}
|
||||||
|
color={'myGray.900'}
|
||||||
|
onChange={(e) => {
|
||||||
|
const textarea = e.target;
|
||||||
|
textarea.style.height = textareaMinH;
|
||||||
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// 触发快捷发送
|
||||||
|
if (e.keyCode === 13 && !e.shiftKey) {
|
||||||
|
handleSubmit(sendPrompt)();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
// 全选内容
|
||||||
|
// @ts-ignore
|
||||||
|
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 发送和等待按键 */}
|
||||||
|
<Flex
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'center'}
|
||||||
|
h={'25px'}
|
||||||
|
w={'25px'}
|
||||||
|
position={'absolute'}
|
||||||
|
right={['12px', '20px']}
|
||||||
|
bottom={'15px'}
|
||||||
|
>
|
||||||
|
{isChatting ? (
|
||||||
|
<MyIcon
|
||||||
|
className={styles.stopIcon}
|
||||||
|
width={['22px', '25px']}
|
||||||
|
height={['22px', '25px']}
|
||||||
|
cursor={'pointer'}
|
||||||
|
name={'stop'}
|
||||||
|
color={'gray.500'}
|
||||||
|
onClick={() => controller.current?.abort()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MyIcon
|
||||||
|
name={'chatSend'}
|
||||||
|
width={['18px', '20px']}
|
||||||
|
height={['18px', '20px']}
|
||||||
|
cursor={'pointer'}
|
||||||
|
color={'gray.500'}
|
||||||
|
onClick={handleSubmit(sendPrompt)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(forwardRef(ChatBox));
|
||||||
@@ -1 +1 @@
|
|||||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805221456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M267.3 834.6h-96.5c-27.4 0-49.7-22.3-49.7-49.7V115.2c0-27.4 22.3-49.7 49.7-49.7H727c27.4 0 49.7 22.3 49.7 49.7v96.5h-42.6v-96.5c0-3.9-3.2-7.1-7.1-7.1H170.8c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h96.5v42.6z" p-id="1174"></path><path d="M851.9 959.5H295.7c-27.4 0-49.7-22.3-49.7-49.7V240.1c0-27.4 22.3-49.7 49.7-49.7h556.2c27.4 0 49.7 22.3 49.7 49.7v669.7c-0.1 27.4-22.3 49.7-49.7 49.7zM295.7 233c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h556.2c3.9 0 7.1-3.2 7.1-7.1V240.1c0-3.9-3.2-7.1-7.1-7.1H295.7z" p-id="1175"></path></svg>
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1689057990782" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1770" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M878.5 255.1H770V146.7c0-43.6-35.5-79.1-79.1-79.1H145.2c-43.6 0-79.1 35.5-79.1 79.1v545.8c0 43.6 35.5 79.1 79.1 79.1h108.4V880c0 43.6 35.5 79.1 79.1 79.1h545.8c43.6 0 79.1-35.5 79.1-79.1V334.2c-0.1-43.6-35.6-79.1-79.1-79.1zM145.2 707.5c-8.3 0-15.1-6.8-15.1-15.1V146.7c0-8.3 6.8-15.1 15.1-15.1H691c8.3 0 15.1 6.8 15.1 15.1v545.8c0 8.3-6.8 15.1-15.1 15.1H145.2zM893.5 880c0 8.3-6.8 15.1-15.1 15.1H332.7c-8.3 0-15.1-6.8-15.1-15.1V771.5H691c43.6 0 79.1-35.5 79.1-79.1V319.1h108.4c8.3 0 15.1 6.8 15.1 15.1V880z" p-id="1771"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 840 B |
1
client/src/components/Icon/icons/light/setTop.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688993655221" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1709" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M354.7912723 841.95191467l94.44951229 94.4495123c13.67032415 13.67032415 26.71926992 19.88410785 47.22475616 19.88410785 13.67032415 0 33.554432-6.83516208 47.22475614-19.88410785l19.88410786-20.50548622c20.50548622-19.88410785 33.554432-47.22475615 33.554432-73.94402608v-60.27370192c0-6.83516208 6.83516208-26.71926992 13.67032414-33.554432l175.22870044-175.22870045c6.83516208-6.83516208 19.88410785-13.67032415 33.554432-13.67032415h60.27370192c26.71926992 0 60.27370192-13.67032415 73.94402608-33.554432l20.50548622-19.88410785c6.83516208-6.83516208 13.67032415-19.88410785 13.67032415-40.38959408 0-13.67032415-6.83516208-33.554432-19.88410785-47.22475614l-329.95191467-329.95191467c-13.67032415-13.67032415-26.71926992-19.88410785-47.22475615-19.88410786-13.67032415 0-33.554432 6.83516208-47.22475614 19.88410786l-13.67032414 20.50548622c-20.50548622 19.88410785-33.554432 54.05991822-33.554432 73.94402607v60.27370193c0 6.83516208-6.83516208 26.71926992-13.67032416 33.554432L307.56651615 451.1049197c-6.83516208 6.83516208-19.88410785 13.67032415-26.71926993 13.67032415H213.11700385c-26.71926992 0-60.27370192 13.67032415-73.94402607 33.554432l-19.88410786 19.88410785c-19.88410785 20.50548622-19.88410785 60.27370192 0 87.61435022L213.73838222 700.27764622l141.05289008 141.67426845z m-87.61435022-94.4495123l47.22475614 47.22475615-47.22475614-47.22475615z m639.3983431-262.22167229c-6.83516208 6.83516208-20.50548622 13.67032415-33.554432 13.67032414h-60.27370193c-26.71926992 0-60.27370192 13.67032415-73.94402607 33.554432l-175.22870043 175.22870045c-19.88410785 19.88410785-33.554432 47.22475615-33.554432 73.94402608v60.27370192c0 6.83516208-6.83516208 26.71926992-13.67032416 33.554432l-19.88410784 19.88410785-329.95191467-336.78707674 19.88410784-20.50548623c6.83516208 0 19.88410785-6.83516208 26.71926993-6.83516207h60.27370193c26.71926992 0 60.27370192-13.67032415 73.94402607-33.554432l175.22870045-175.22870045c26.71926992-20.50548622 40.38959408-54.05991822 40.38959407-73.94402606V182.04808533c0-6.83516208 6.83516208-26.71926992 13.67032415-33.554432l13.67032415-13.67032415 329.95191466 329.95191467-13.67032415 20.50548623z" p-id="1710"></path><path d="M269.04105718 753.0948077l33.554432 33.554432-164.66526815 164.66526815-33.554432-33.554432c0.62137837 0 164.66526815-164.66526815 164.66526815-164.66526815z" p-id="1711"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
1
client/src/components/Icon/icons/modules/variable.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688977884412" class="icon" viewBox="0 0 1682 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12489" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M906.971429 548.571429l234.057142 234.057142c29.257143 29.257143 29.257143 73.142857 0 102.4-29.257143 29.257143-73.142857 29.257143-102.4 0L804.571429 650.971429l-234.057143 234.057142c-29.257143 29.257143-73.142857 29.257143-102.4 0s-29.257143-73.142857 0-102.4l234.057143-234.057142-234.057143-234.057143c-29.257143-29.257143-29.257143-73.142857 0-102.4s73.142857-29.257143 102.4 0L804.571429 446.171429l234.057142-234.057143c29.257143-29.257143 73.142857-29.257143 102.4 0s29.257143 73.142857 0 102.4l-234.057142 234.057143zM256 980.114286c-14.628571 0-36.571429-7.314286-51.2-21.942857C80.457143 841.142857 0 665.6 0 490.057143 0 307.2 73.142857 146.285714 204.8 21.942857c29.257143-29.257143 73.142857-29.257143 102.4 0 29.257143 29.257143 29.257143 73.142857 0 102.4C204.8 219.428571 146.285714 351.085714 146.285714 490.057143c0 131.657143 58.514286 270.628571 160.914286 365.714286 29.257143 29.257143 29.257143 73.142857 0 102.4-14.628571 14.628571-29.257143 21.942857-51.2 21.942857z m1104.457143-7.314286c-21.942857 0-36.571429-7.314286-51.2-21.942857-29.257143-29.257143-29.257143-73.142857 0-102.4 102.4-95.085714 160.914286-226.742857 160.914286-365.714286s-58.514286-263.314286-160.914286-351.085714c-29.257143-29.257143-36.571429-73.142857-7.314286-102.4 29.257143-29.257143 73.142857-36.571429 102.4-7.314286C1536 138.971429 1609.142857 307.2 1609.142857 490.057143c0 182.857143-73.142857 343.771429-204.8 468.114286-7.314286 7.314286-29.257143 14.628571-43.885714 14.628571z" p-id="12490"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
1
client/src/components/Icon/icons/modules/welcomeText.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688977803658" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8763" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512.000557 0c282.779765 0 511.999889 229.220124 511.999888 511.999889 0 271.404463-211.166563 493.478849-478.163374 510.886845l1.068521 1.113043H60.03822a13.356519 13.356519 0 0 1-10.885563-21.12556c37.509557-52.491119 62.241378-105.004499 74.239984-157.517879A509.951889 509.951889 0 0 1 0.000668 511.999889C0.000668 229.220124 229.220792 0 512.000557 0z m0 80.139113C273.497652 80.139113 80.139781 273.496984 80.139781 511.999889c0 101.286935 34.905036 197.008653 97.680674 273.563766l6.366608 7.568694 26.312342 30.675472-8.971129 39.40173a458.2399 458.2399 0 0 1-25.377386 77.623636l-1.335651 3.027477h351.476793l14.335997-0.934956c223.610386-14.580866 399.248609-198.856305 403.166521-423.268082L943.861332 511.999889c0-238.502905-193.357871-431.860776-431.860775-431.860776z m109.078237 205.601347c90.735285 0 163.72866 75.553375 163.72866 168.069528 0 59.102596-30.007646 113.085193-78.358244 143.449013l4.140521-2.715826-122.323452 124.727625a106.896672 106.896672 0 0 1-143.40449 8.281042l-4.051478-3.428173-3.917912-3.673043-123.725886-126.063277-5.943651-4.185043c-40.626078-30.052167-66.003464-77.779461-67.895638-130.070232l-0.133565-6.322086c0-92.516154 72.993375-168.069529 163.7064-168.069528 40.959991 0 79.137374 15.560344 108.365889 42.073034l0.734609 0.667826 5.721042-5.008695a160.834748 160.834748 0 0 1 97.257718-37.620861z m0 80.139113c-30.853559 0-58.879987 17.786431-73.438593 46.035468-14.914779 28.939124-56.275466 28.983646-71.234767 0.044522l-2.270608-4.162782c-15.137388-25.867125-41.917208-41.917208-71.234767-41.917208-45.83512 0-83.567286 39.067818-83.567287 87.930415 0 31.454602 15.760692 59.814944 40.804165 75.553375l2.582261 1.780869a40.069557 40.069557 0 0 1 4.674781 4.095999l125.88519 128.267103a26.713038 26.713038 0 0 0 37.776688-0.356174l125.462233-127.888668a40.069557 40.069557 0 0 1 7.301564-5.854607l4.095999-2.738086c22.66156-16.183649 36.730427-43.141556 36.730427-72.859811 0-48.862598-37.709905-87.930416-83.545025-87.930415z" p-id="8764"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
1
client/src/components/Icon/icons/voice.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1689059818626" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2851" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M370.675057 293.943709c-20.172608-25.93621-17.290806-66.281425 8.645403-86.454032 25.93621-23.054409 66.281425-17.290806 86.454032 8.645403 69.163226 83.572231 103.744839 190.198871 103.744839 299.707312 0 106.62664-34.581613 213.253279-103.744839 296.82551-20.172608 25.93621-60.517823 31.699812-86.454032 8.645403-25.93621-20.172608-28.818011-60.517823-8.645403-86.454032 48.990618-60.517823 74.926828-138.326451 74.926827-219.016881s-25.93621-161.38086-74.926827-221.898683z m288.180107-184.435268c-23.054409-25.93621-20.172608-66.281425 8.645403-89.335833s69.163226-20.172608 92.217634 8.645403c109.508441 135.44465 164.262661 311.234516 164.262661 487.024381 0 172.908064-54.75422 348.69793-164.262661 484.14258-23.054409 28.818011-63.399624 31.699812-92.217634 8.645403-28.818011-20.172608-31.699812-63.399624-8.645403-89.335833 89.335833-112.390242 135.44465-259.362096 135.44465-403.45215 0-146.971855-46.108817-293.943709-135.44465-406.333951zM108.431159 449.560967c-17.290806-25.93621-14.409005-60.517823 8.645403-80.69043 23.054409-17.290806 60.517823-14.409005 77.808629 8.645403 31.699812 37.463414 48.990618 89.335833 48.990618 138.326452s-17.290806 97.981236-48.990618 135.44465c-17.290806 23.054409-54.75422 28.818011-77.808629 8.645403s-25.93621-54.75422-8.645403-77.808629c14.409005-20.172608 23.054409-43.227016 23.054409-66.281424 0-25.93621-8.645403-48.990618-23.054409-66.281425z" p-id="2852"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -46,12 +46,19 @@ const map = {
|
|||||||
appLight: require('./icons/light/app.svg').default,
|
appLight: require('./icons/light/app.svg').default,
|
||||||
appFill: require('./icons/fill/app.svg').default,
|
appFill: require('./icons/fill/app.svg').default,
|
||||||
meLight: require('./icons/light/me.svg').default,
|
meLight: require('./icons/light/me.svg').default,
|
||||||
meFill: require('./icons/fill/me.svg').default
|
meFill: require('./icons/fill/me.svg').default,
|
||||||
|
welcomeText: require('./icons/modules/welcomeText.svg').default,
|
||||||
|
variable: require('./icons/modules/variable.svg').default,
|
||||||
|
setTop: require('./icons/light/setTop.svg').default,
|
||||||
|
voice: require('./icons/voice.svg').default
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconName = keyof typeof map;
|
export type IconName = keyof typeof map;
|
||||||
|
|
||||||
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
|
const MyIcon = (
|
||||||
|
{ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps,
|
||||||
|
ref: any
|
||||||
|
) => {
|
||||||
return map[name] ? (
|
return map[name] ? (
|
||||||
<Icon
|
<Icon
|
||||||
as={map[name]}
|
as={map[name]}
|
||||||
@@ -65,4 +72,4 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } &
|
|||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MyIcon;
|
export default React.forwardRef(MyIcon);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const MyTooltip = ({ children, ...props }: TooltipProps) => {
|
|||||||
py={2}
|
py={2}
|
||||||
borderRadius={'8px'}
|
borderRadius={'8px'}
|
||||||
whiteSpace={'pre-wrap'}
|
whiteSpace={'pre-wrap'}
|
||||||
|
shouldWrapChildren
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuButton,
|
|
||||||
Button,
|
Button,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
useOutsideClick
|
useOutsideClick
|
||||||
@@ -44,7 +43,13 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu autoSelect={false} isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
<Menu autoSelect={false} isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||||
<Box ref={SelectRef} position={'relative'} onClick={() => (isOpen ? onClose() : onOpen())}>
|
<Box
|
||||||
|
ref={SelectRef}
|
||||||
|
position={'relative'}
|
||||||
|
onClick={() => {
|
||||||
|
isOpen ? onClose() : onOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
width={width}
|
width={width}
|
||||||
@@ -65,6 +70,7 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{list.find((item) => item.value === value)?.label || placeholder}
|
{list.find((item) => item.value === value)?.label || placeholder}
|
||||||
|
<Box flex={1} />
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
|
|||||||
import { Box, type BoxProps } from '@chakra-ui/react';
|
import { Box, type BoxProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
interface Props extends BoxProps {
|
interface Props extends BoxProps {
|
||||||
children: string;
|
children: React.ReactNode | React.ReactNode[];
|
||||||
colorSchema?: 'blue' | 'green' | 'gray';
|
colorSchema?: 'blue' | 'green' | 'gray';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
|
|||||||
border={'1px solid'}
|
border={'1px solid'}
|
||||||
px={2}
|
px={2}
|
||||||
lineHeight={1}
|
lineHeight={1}
|
||||||
py={'2px'}
|
py={1}
|
||||||
borderRadius={'md'}
|
borderRadius={'md'}
|
||||||
fontSize={'xs'}
|
fontSize={'xs'}
|
||||||
{...theme}
|
{...theme}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import type { AppItemType } from '@/types/app';
|
|||||||
|
|
||||||
/* app */
|
/* app */
|
||||||
export enum AppModuleItemTypeEnum {
|
export enum AppModuleItemTypeEnum {
|
||||||
|
'userGuide' = 'userGuide', // default chat input: userChatInput, history
|
||||||
'initInput' = 'initInput', // default chat input: userChatInput, history
|
'initInput' = 'initInput', // default chat input: userChatInput, history
|
||||||
'http' = 'http', // send a http request
|
'http' = 'http', // send a http request
|
||||||
'switch' = 'switch', // one input and two outputs
|
'switch' = 'switch', // one input and two outputs
|
||||||
'answer' = 'answer' // redirect response
|
'answer' = 'answer' // redirect response
|
||||||
}
|
}
|
||||||
export enum SystemInputEnum {
|
export enum SystemInputEnum {
|
||||||
|
'welcomeText' = 'welcomeText',
|
||||||
|
'variables' = 'variables',
|
||||||
'switch' = 'switch', // a trigger switch
|
'switch' = 'switch', // a trigger switch
|
||||||
'history' = 'history',
|
'history' = 'history',
|
||||||
'userChatInput' = 'userChatInput'
|
'userChatInput' = 'userChatInput'
|
||||||
@@ -15,6 +18,10 @@ export enum SystemInputEnum {
|
|||||||
export enum SpecificInputEnum {
|
export enum SpecificInputEnum {
|
||||||
'answerText' = 'answerText' // answer module text key
|
'answerText' = 'answerText' // answer module text key
|
||||||
}
|
}
|
||||||
|
export enum VariableInputEnum {
|
||||||
|
input = 'input',
|
||||||
|
select = 'select'
|
||||||
|
}
|
||||||
|
|
||||||
// template
|
// template
|
||||||
export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = [
|
export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = [
|
||||||
|
|||||||
@@ -8,6 +8,27 @@ import {
|
|||||||
Input_Template_UserChatInput
|
Input_Template_UserChatInput
|
||||||
} from './inputTemplate';
|
} from './inputTemplate';
|
||||||
|
|
||||||
|
export const VariableInputModule: AppModuleTemplateItemType = {
|
||||||
|
logo: '/imgs/module/userGuide.png',
|
||||||
|
name: '开场引导',
|
||||||
|
intro: '可以在每个新对话开始前,给用户发送一段开场白,或要求用户填写一些内容作为本轮对话的变量。',
|
||||||
|
type: AppModuleItemTypeEnum.userGuide,
|
||||||
|
flowType: FlowModuleTypeEnum.userGuide,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.welcomeText,
|
||||||
|
type: FlowInputItemTypeEnum.input,
|
||||||
|
label: '开场白'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SystemInputEnum.variables,
|
||||||
|
type: FlowInputItemTypeEnum.systemInput,
|
||||||
|
label: '变量输入',
|
||||||
|
value: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outputs: []
|
||||||
|
};
|
||||||
export const UserInputModule: AppModuleTemplateItemType = {
|
export const UserInputModule: AppModuleTemplateItemType = {
|
||||||
logo: '/imgs/module/userChatInput.png',
|
logo: '/imgs/module/userChatInput.png',
|
||||||
name: '用户问题',
|
name: '用户问题',
|
||||||
@@ -311,7 +332,7 @@ export const ClassifyQuestionModule: AppModuleTemplateItemType = {
|
|||||||
export const ModuleTemplates = [
|
export const ModuleTemplates = [
|
||||||
{
|
{
|
||||||
label: '输入模块',
|
label: '输入模块',
|
||||||
list: [UserInputModule, HistoryModule]
|
list: [UserInputModule, HistoryModule, VariableInputModule]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '对话模块',
|
label: '对话模块',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export enum FlowInputItemTypeEnum {
|
export enum FlowInputItemTypeEnum {
|
||||||
systemInput = 'systemInput', // history, userChatInput
|
systemInput = 'systemInput', // history, userChatInput, variableInput
|
||||||
input = 'input',
|
input = 'input',
|
||||||
textarea = 'textarea',
|
textarea = 'textarea',
|
||||||
numberInput = 'numberInput',
|
numberInput = 'numberInput',
|
||||||
@@ -19,6 +19,7 @@ export enum FlowOutputItemTypeEnum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum FlowModuleTypeEnum {
|
export enum FlowModuleTypeEnum {
|
||||||
|
userGuide = 'userGuide',
|
||||||
questionInputNode = 'questionInput',
|
questionInputNode = 'questionInput',
|
||||||
historyNode = 'historyNode',
|
historyNode = 'historyNode',
|
||||||
chatNode = 'chatNode',
|
chatNode = 'chatNode',
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ const Textarea: ComponentStyleConfig = {
|
|||||||
borderColor: 'myGray.200',
|
borderColor: 'myGray.200',
|
||||||
_focus: {
|
_focus: {
|
||||||
borderColor: 'myBlue.600',
|
borderColor: 'myBlue.600',
|
||||||
boxShadow: '0px 0px 4px #A8DBFF'
|
boxShadow: '0px 0px 4px #A8DBFF',
|
||||||
|
bg: 'white'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,367 +0,0 @@
|
|||||||
import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
|
|
||||||
import { throttle } from 'lodash';
|
|
||||||
import { ChatSiteItemType } from '@/types/chat';
|
|
||||||
import { useToast } from './useToast';
|
|
||||||
import { useCopyData } from '@/utils/tools';
|
|
||||||
import { Box, Card, Flex, Textarea } from '@chakra-ui/react';
|
|
||||||
import { useUserStore } from '@/store/user';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
import { Types } from 'mongoose';
|
|
||||||
import { HUMAN_ICON } from '@/constants/chat';
|
|
||||||
import Markdown from '@/components/Markdown';
|
|
||||||
import MyIcon from '@/components/Icon';
|
|
||||||
import Avatar from '@/components/Avatar';
|
|
||||||
|
|
||||||
import styles from './useChat.module.scss';
|
|
||||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
|
||||||
import { streamFetch } from '@/api/fetch';
|
|
||||||
|
|
||||||
const textareaMinH = '22px';
|
|
||||||
|
|
||||||
export const useChat = ({ appId }: { appId: string }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const ChatBoxParentRef = useRef<HTMLDivElement>(null);
|
|
||||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
// stop chat
|
|
||||||
const controller = useRef(new AbortController());
|
|
||||||
const isLeavePage = useRef(false);
|
|
||||||
|
|
||||||
const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]);
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { copyData } = useCopyData();
|
|
||||||
const { userInfo } = useUserStore();
|
|
||||||
|
|
||||||
const isChatting = useMemo(
|
|
||||||
() => chatHistory[chatHistory.length - 1]?.status === 'loading',
|
|
||||||
[chatHistory]
|
|
||||||
);
|
|
||||||
const isLargeWidth =
|
|
||||||
ChatBoxParentRef?.current?.clientWidth && ChatBoxParentRef?.current?.clientWidth > 900;
|
|
||||||
|
|
||||||
// 滚动到底部
|
|
||||||
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
|
|
||||||
if (!ChatBoxParentRef.current) return;
|
|
||||||
console.log(ChatBoxParentRef.current.scrollHeight);
|
|
||||||
|
|
||||||
ChatBoxParentRef.current.scrollTo({
|
|
||||||
top: ChatBoxParentRef.current.scrollHeight,
|
|
||||||
behavior
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const generatingMessage = useCallback(
|
|
||||||
throttle(() => {
|
|
||||||
if (!ChatBoxParentRef.current) return;
|
|
||||||
const isBottom =
|
|
||||||
ChatBoxParentRef.current.scrollTop + ChatBoxParentRef.current.clientHeight + 150 >=
|
|
||||||
ChatBoxParentRef.current.scrollHeight;
|
|
||||||
|
|
||||||
isBottom && scrollToBottom('auto');
|
|
||||||
}, 100),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 复制内容
|
|
||||||
const onclickCopy = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const val = value.replace(/\n+/g, '\n');
|
|
||||||
copyData(val);
|
|
||||||
},
|
|
||||||
[copyData]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 重置输入内容
|
|
||||||
const resetInputVal = useCallback((val: string) => {
|
|
||||||
if (!TextareaDom.current) return;
|
|
||||||
TextareaDom.current.value = val;
|
|
||||||
setTimeout(() => {
|
|
||||||
/* 回到最小高度 */
|
|
||||||
if (TextareaDom.current) {
|
|
||||||
TextareaDom.current.style.height =
|
|
||||||
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const startChat = useCallback(
|
|
||||||
async (prompts: ChatSiteItemType[]) => {
|
|
||||||
// create abort obj
|
|
||||||
const abortSignal = new AbortController();
|
|
||||||
controller.current = abortSignal;
|
|
||||||
isLeavePage.current = false;
|
|
||||||
|
|
||||||
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
|
|
||||||
|
|
||||||
// 流请求,获取数据
|
|
||||||
await streamFetch({
|
|
||||||
data: {
|
|
||||||
messages,
|
|
||||||
appId,
|
|
||||||
model: ''
|
|
||||||
},
|
|
||||||
onMessage: (text: string) => {
|
|
||||||
setChatHistory((state) =>
|
|
||||||
state.map((item, index) => {
|
|
||||||
if (index !== state.length - 1) return item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
value: item.value + text
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
generatingMessage();
|
|
||||||
},
|
|
||||||
abortSignal
|
|
||||||
});
|
|
||||||
|
|
||||||
// 重置了页面,说明退出了当前聊天, 不缓存任何内容
|
|
||||||
if (isLeavePage.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置聊天内容为完成状态
|
|
||||||
setChatHistory((state) =>
|
|
||||||
state.map((item, index) => {
|
|
||||||
if (index !== state.length - 1) return item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
status: 'finish'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
generatingMessage();
|
|
||||||
TextareaDom.current?.focus();
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
[appId, generatingMessage]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* user confirm send prompt
|
|
||||||
*/
|
|
||||||
const sendPrompt = useCallback(async () => {
|
|
||||||
if (isChatting) {
|
|
||||||
toast({
|
|
||||||
title: '正在聊天中...请等待结束',
|
|
||||||
status: 'warning'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// get input value
|
|
||||||
const value = TextareaDom.current?.value || '';
|
|
||||||
const val = value.trim().replace(/\n\s*/g, '\n');
|
|
||||||
|
|
||||||
if (!val) {
|
|
||||||
toast({
|
|
||||||
title: '内容为空',
|
|
||||||
status: 'warning'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newChatList: ChatSiteItemType[] = [
|
|
||||||
...chatHistory,
|
|
||||||
{
|
|
||||||
_id: String(new Types.ObjectId()),
|
|
||||||
obj: 'Human',
|
|
||||||
value: val,
|
|
||||||
status: 'finish'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: String(new Types.ObjectId()),
|
|
||||||
obj: 'AI',
|
|
||||||
value: '',
|
|
||||||
status: 'loading'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 插入内容
|
|
||||||
setChatHistory(newChatList);
|
|
||||||
|
|
||||||
// 清空输入内容
|
|
||||||
resetInputVal('');
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await startChat(newChatList);
|
|
||||||
} catch (err: any) {
|
|
||||||
toast({
|
|
||||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
|
||||||
status: 'warning',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
resetInputVal(value);
|
|
||||||
|
|
||||||
setChatHistory(newChatList.slice(0, newChatList.length - 2));
|
|
||||||
}
|
|
||||||
}, [isChatting, chatHistory, resetInputVal, toast, scrollToBottom, startChat]);
|
|
||||||
|
|
||||||
const ChatBox = useCallback(
|
|
||||||
({ appAvatar }: { appAvatar: string }) => {
|
|
||||||
return (
|
|
||||||
<Box id={'history'}>
|
|
||||||
{chatHistory.map((item, index) => (
|
|
||||||
<Flex key={item._id} alignItems={'flex-start'} py={2}>
|
|
||||||
{item.obj === 'Human' && <Box flex={1} />}
|
|
||||||
{/* avatar */}
|
|
||||||
<Avatar
|
|
||||||
src={item.obj === 'Human' ? userInfo?.avatar || HUMAN_ICON : appAvatar}
|
|
||||||
w={isLargeWidth ? '34px' : '24px'}
|
|
||||||
h={isLargeWidth ? '34px' : '24px'}
|
|
||||||
{...(item.obj === 'AI'
|
|
||||||
? {
|
|
||||||
order: 1,
|
|
||||||
mr: ['6px', 2]
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
order: 3,
|
|
||||||
ml: ['6px', 2]
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{/* message */}
|
|
||||||
<Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
|
||||||
{item.obj === 'AI' ? (
|
|
||||||
<Box w={'100%'}>
|
|
||||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
|
||||||
<Markdown
|
|
||||||
source={item.value}
|
|
||||||
isChatting={index === chatHistory.length - 1 && isChatting}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box>
|
|
||||||
<Card
|
|
||||||
className="markdown"
|
|
||||||
whiteSpace={'pre-wrap'}
|
|
||||||
px={4}
|
|
||||||
py={3}
|
|
||||||
borderRadius={'8px 0 8px 8px'}
|
|
||||||
bg={'myBlue.300'}
|
|
||||||
>
|
|
||||||
<Box as={'p'}>{item.value}</Box>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[chatHistory, isChatting, userInfo?.avatar]
|
|
||||||
);
|
|
||||||
|
|
||||||
const ChatInput = useCallback(() => {
|
|
||||||
return (
|
|
||||||
<Box m={['0 auto', '20px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']}>
|
|
||||||
<Box
|
|
||||||
py={'18px'}
|
|
||||||
position={'relative'}
|
|
||||||
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
|
|
||||||
borderTop={['1px solid', 0]}
|
|
||||||
borderTopColor={'gray.200'}
|
|
||||||
borderRadius={['none', 'md']}
|
|
||||||
backgroundColor={'white'}
|
|
||||||
>
|
|
||||||
{/* 输入框 */}
|
|
||||||
<Textarea
|
|
||||||
ref={TextareaDom}
|
|
||||||
py={0}
|
|
||||||
pr={['45px', '55px']}
|
|
||||||
border={'none'}
|
|
||||||
_focusVisible={{
|
|
||||||
border: 'none'
|
|
||||||
}}
|
|
||||||
placeholder="提问"
|
|
||||||
resize={'none'}
|
|
||||||
rows={1}
|
|
||||||
height={'22px'}
|
|
||||||
lineHeight={'22px'}
|
|
||||||
maxHeight={'150px'}
|
|
||||||
maxLength={-1}
|
|
||||||
overflowY={'auto'}
|
|
||||||
whiteSpace={'pre-wrap'}
|
|
||||||
wordBreak={'break-all'}
|
|
||||||
boxShadow={'none !important'}
|
|
||||||
color={'myGray.900'}
|
|
||||||
onChange={(e) => {
|
|
||||||
const textarea = e.target;
|
|
||||||
textarea.style.height = textareaMinH;
|
|
||||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// 触发快捷发送
|
|
||||||
if (e.keyCode === 13 && !e.shiftKey) {
|
|
||||||
sendPrompt();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
// 全选内容
|
|
||||||
// @ts-ignore
|
|
||||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* 发送和等待按键 */}
|
|
||||||
<Flex
|
|
||||||
alignItems={'center'}
|
|
||||||
justifyContent={'center'}
|
|
||||||
h={'25px'}
|
|
||||||
w={'25px'}
|
|
||||||
position={'absolute'}
|
|
||||||
right={['12px', '20px']}
|
|
||||||
bottom={'15px'}
|
|
||||||
>
|
|
||||||
{isChatting ? (
|
|
||||||
<MyIcon
|
|
||||||
className={styles.stopIcon}
|
|
||||||
width={['22px', '25px']}
|
|
||||||
height={['22px', '25px']}
|
|
||||||
cursor={'pointer'}
|
|
||||||
name={'stop'}
|
|
||||||
color={'gray.500'}
|
|
||||||
onClick={() => controller.current?.abort()}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<MyIcon
|
|
||||||
name={'chatSend'}
|
|
||||||
width={['18px', '20px']}
|
|
||||||
height={['18px', '20px']}
|
|
||||||
cursor={'pointer'}
|
|
||||||
color={'gray.500'}
|
|
||||||
onClick={sendPrompt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}, [isChatting, sendPrompt]);
|
|
||||||
|
|
||||||
// abort stream
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
window.speechSynthesis?.cancel();
|
|
||||||
isLeavePage.current = true;
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
controller.current?.abort();
|
|
||||||
};
|
|
||||||
}, [router.asPath]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ChatBoxParentRef,
|
|
||||||
scrollToBottom,
|
|
||||||
setChatHistory,
|
|
||||||
ChatBox,
|
|
||||||
ChatInput
|
|
||||||
};
|
|
||||||
};
|
|
||||||
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 { connectToDatabase, ShareChat } from '@/service/mongo';
|
||||||
import { authApp, authUser } from '@/service/utils/auth';
|
import { authApp, authUser } from '@/service/utils/auth';
|
||||||
import type { ShareChatEditType } from '@/types/app';
|
import type { ShareChatEditType } from '@/types/app';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||||
|
|
||||||
/* create a shareChat */
|
/* create a shareChat */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { modelId, name, maxContext, password } = req.body as ShareChatEditType & {
|
const { appId, name, maxContext } = req.body as ShareChatEditType & {
|
||||||
modelId: string;
|
appId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
await authApp({
|
await authApp({
|
||||||
appId: modelId,
|
appId,
|
||||||
userId,
|
userId,
|
||||||
authOwner: false
|
authOwner: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const { _id } = await ShareChat.create({
|
const shareId = nanoid();
|
||||||
|
await ShareChat.create({
|
||||||
|
shareId,
|
||||||
userId,
|
userId,
|
||||||
modelId,
|
appId,
|
||||||
name,
|
name,
|
||||||
password,
|
|
||||||
maxContext
|
maxContext
|
||||||
});
|
});
|
||||||
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
data: _id
|
data: shareId
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import type { InitShareChatResponse } from '@/api/response/chat';
|
|||||||
import { authApp } from '@/service/utils/auth';
|
import { authApp } from '@/service/utils/auth';
|
||||||
import { hashPassword } from '@/service/utils/tools';
|
import { hashPassword } from '@/service/utils/tools';
|
||||||
import { HUMAN_ICON } from '@/constants/chat';
|
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) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
let { shareId, password = '' } = req.query as {
|
let { shareId } = req.query as {
|
||||||
shareId: string;
|
shareId: string;
|
||||||
password: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!shareId) {
|
if (!shareId) {
|
||||||
@@ -21,22 +22,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// get shareChat
|
// get shareChat
|
||||||
const shareChat = await ShareChat.findById(shareId);
|
const shareChat = await ShareChat.findOne({ shareId });
|
||||||
|
|
||||||
if (!shareChat) {
|
if (!shareChat) {
|
||||||
throw new Error('分享链接已失效');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shareChat.password !== hashPassword(password)) {
|
|
||||||
return jsonRes(res, {
|
return jsonRes(res, {
|
||||||
code: 501,
|
code: 501,
|
||||||
message: '密码不正确'
|
error: '分享链接已失效'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验使用权限
|
// 校验使用权限
|
||||||
const { app } = await authApp({
|
const { app } = await authApp({
|
||||||
appId: shareChat.modelId,
|
appId: shareChat.appId,
|
||||||
userId: String(shareChat.userId),
|
userId: String(shareChat.userId),
|
||||||
authOwner: false
|
authOwner: false
|
||||||
});
|
});
|
||||||
@@ -45,15 +42,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
jsonRes<InitShareChatResponse>(res, {
|
jsonRes<InitShareChatResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
appId: shareChat.modelId,
|
|
||||||
maxContext: shareChat.maxContext,
|
|
||||||
userAvatar: user?.avatar || HUMAN_ICON,
|
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,
|
name: app.name,
|
||||||
avatar: app.avatar,
|
avatar: app.avatar,
|
||||||
intro: app.intro
|
intro: app.intro
|
||||||
},
|
}
|
||||||
chatModel: app.chat.chatModel
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { connectToDatabase, ShareChat } from '@/service/mongo';
|
|||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { hashPassword } from '@/service/utils/tools';
|
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) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { modelId } = req.query as {
|
const { appId } = req.query as {
|
||||||
modelId: string;
|
appId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
@@ -16,19 +16,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
|
|
||||||
const data = await ShareChat.find({
|
const data = await ShareChat.find({
|
||||||
modelId,
|
appId,
|
||||||
userId
|
userId
|
||||||
}).sort({
|
}).sort({
|
||||||
_id: -1
|
_id: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
const blankPassword = hashPassword('');
|
|
||||||
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
data: data.map((item) => ({
|
data: data.map((item) => ({
|
||||||
_id: item._id,
|
_id: item._id,
|
||||||
|
shareId: item.shareId,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
password: item.password === blankPassword ? '' : '1',
|
|
||||||
tokens: item.tokens,
|
tokens: item.tokens,
|
||||||
maxContext: item.maxContext,
|
maxContext: item.maxContext,
|
||||||
lastTime: item.lastTime
|
lastTime: item.lastTime
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export async function chatCompletion({
|
|||||||
|
|
||||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||||
const chatAPI = getOpenAIApi();
|
const chatAPI = getOpenAIApi();
|
||||||
|
console.log(adaptMessages);
|
||||||
|
|
||||||
/* count response max token */
|
/* count response max token */
|
||||||
const promptsToken = modelToolMap.countTokens({
|
const promptsToken = modelToolMap.countTokens({
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ type FastGptWebChatProps = {
|
|||||||
appId?: string;
|
appId?: string;
|
||||||
};
|
};
|
||||||
type FastGptShareChatProps = {
|
type FastGptShareChatProps = {
|
||||||
password?: string;
|
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
};
|
};
|
||||||
export type Props = CreateChatCompletionRequest &
|
export type Props = CreateChatCompletionRequest &
|
||||||
@@ -31,6 +30,7 @@ export type Props = CreateChatCompletionRequest &
|
|||||||
FastGptShareChatProps & {
|
FastGptShareChatProps & {
|
||||||
messages: MessageItemType[];
|
messages: MessageItemType[];
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
|
variables: Record<string, any>;
|
||||||
};
|
};
|
||||||
export type ChatResponseType = {
|
export type ChatResponseType = {
|
||||||
newChatId: string;
|
newChatId: string;
|
||||||
@@ -46,7 +46,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
res.end();
|
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 {
|
try {
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
@@ -66,8 +66,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
authType
|
authType
|
||||||
} = await (shareId
|
} = await (shareId
|
||||||
? authShareChat({
|
? authShareChat({
|
||||||
shareId,
|
shareId
|
||||||
password
|
|
||||||
})
|
})
|
||||||
: authUser({ req }));
|
: authUser({ req }));
|
||||||
|
|
||||||
@@ -105,6 +104,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
const { responseData, answerText } = await dispatchModules({
|
const { responseData, answerText } = await dispatchModules({
|
||||||
res,
|
res,
|
||||||
modules: app.modules,
|
modules: app.modules,
|
||||||
|
variables,
|
||||||
params: {
|
params: {
|
||||||
history: prompts,
|
history: prompts,
|
||||||
userChatInput: prompt.value
|
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,
|
res,
|
||||||
modules,
|
modules,
|
||||||
params = {},
|
params = {},
|
||||||
|
variables = {},
|
||||||
stream = false
|
stream = false
|
||||||
}: {
|
}: {
|
||||||
res: NextApiResponse;
|
res: NextApiResponse;
|
||||||
modules: AppModuleItemType[];
|
modules: AppModuleItemType[];
|
||||||
params?: Record<string, any>;
|
params?: Record<string, any>;
|
||||||
|
variables?: Record<string, any>;
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const runningModules = loadModules(modules);
|
const runningModules = loadModules(modules, variables);
|
||||||
let storeData: Record<string, any> = {};
|
let storeData: Record<string, any> = {};
|
||||||
let responseData: Record<string, any> = {};
|
let responseData: Record<string, any> = {};
|
||||||
let answerText = '';
|
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 modules.map((module) => {
|
||||||
return {
|
return {
|
||||||
moduleId: module.moduleId,
|
moduleId: module.moduleId,
|
||||||
@@ -341,10 +346,25 @@ function loadModules(modules: AppModuleItemType[]): RunningModuleItemType[] {
|
|||||||
url: module.url,
|
url: module.url,
|
||||||
inputs: module.inputs
|
inputs: module.inputs
|
||||||
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
|
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
|
||||||
.map((item) => ({
|
.map((item) => {
|
||||||
key: item.key,
|
if (typeof item.value !== 'string') {
|
||||||
value: item.value
|
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) => ({
|
outputs: module.outputs.map((item) => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
answer: item.key === SpecificInputEnum.answerText,
|
answer: item.key === SpecificInputEnum.answerText,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
|
|||||||
ssr: false
|
ssr: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const API = ({ modelId }: { modelId: string }) => {
|
const API = ({ appId }: { appId: string }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { copyData } = useCopyData();
|
const { copyData } = useCopyData();
|
||||||
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
|
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
|
||||||
@@ -33,9 +33,9 @@ const API = ({ modelId }: { modelId: string }) => {
|
|||||||
ml={2}
|
ml={2}
|
||||||
fontWeight={'bold'}
|
fontWeight={'bold'}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
onClick={() => copyData(modelId, '已复制 AppId')}
|
onClick={() => copyData(appId, '已复制 AppId')}
|
||||||
>
|
>
|
||||||
{modelId}
|
{appId}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type { AppSchema } from '@/types/mongoSchema';
|
|||||||
|
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
|
|
||||||
const Settings = ({ modelId }: { modelId: string }) => {
|
const Settings = ({ appId }: { appId: string }) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { Loading, setIsLoading } = useLoading();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
@@ -136,10 +136,10 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// load model data
|
// load model data
|
||||||
const { isLoading } = useQuery([modelId], () => loadAppDetail(modelId, true), {
|
const { isLoading } = useQuery([appId], () => loadAppDetail(appId, true), {
|
||||||
onSuccess(res) {
|
onSuccess(res) {
|
||||||
res && reset(res);
|
res && reset(res);
|
||||||
modelId && setLastModelId(modelId);
|
appId && setLastModelId(appId);
|
||||||
setRefresh(!refresh);
|
setRefresh(!refresh);
|
||||||
},
|
},
|
||||||
onError(err: any) {
|
onError(err: any) {
|
||||||
@@ -240,7 +240,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
|||||||
router.prefetch('/chat');
|
router.prefetch('/chat');
|
||||||
await saveUpdateModel();
|
await saveUpdateModel();
|
||||||
} catch (error) {}
|
} 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 type { ShareChatEditType } from '@/types/app';
|
||||||
import MyTooltip from '@/components/MyTooltip';
|
import MyTooltip from '@/components/MyTooltip';
|
||||||
|
|
||||||
const Share = ({ modelId }: { modelId: string }) => {
|
const Share = ({ appId }: { appId: string }) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { Loading, setIsLoading } = useLoading();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
const { copyData } = useCopyData();
|
const { copyData } = useCopyData();
|
||||||
@@ -63,7 +63,7 @@ const Share = ({ modelId }: { modelId: string }) => {
|
|||||||
isFetching,
|
isFetching,
|
||||||
data: shareChatList = [],
|
data: shareChatList = [],
|
||||||
refetch: refetchShareChatList
|
refetch: refetchShareChatList
|
||||||
} = useQuery(['initShareChatList', modelId], () => getShareChatList(modelId));
|
} = useQuery(['initShareChatList', appId], () => getShareChatList(appId));
|
||||||
|
|
||||||
const onclickCreateShareChat = useCallback(
|
const onclickCreateShareChat = useCallback(
|
||||||
async (e: ShareChatEditType) => {
|
async (e: ShareChatEditType) => {
|
||||||
@@ -71,13 +71,12 @@ const Share = ({ modelId }: { modelId: string }) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const id = await createShareChat({
|
const id = await createShareChat({
|
||||||
...e,
|
...e,
|
||||||
modelId
|
appId
|
||||||
});
|
});
|
||||||
onCloseCreateShareChat();
|
onCloseCreateShareChat();
|
||||||
refetchShareChatList();
|
refetchShareChatList();
|
||||||
|
|
||||||
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}
|
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}`;
|
||||||
${e.password ? `密码为: ${e.password}` : ''}`;
|
|
||||||
copyData(url, '已复制分享地址');
|
copyData(url, '已复制分享地址');
|
||||||
|
|
||||||
resetShareChat(defaultShareChat);
|
resetShareChat(defaultShareChat);
|
||||||
@@ -91,8 +90,8 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
appId,
|
||||||
copyData,
|
copyData,
|
||||||
modelId,
|
|
||||||
onCloseCreateShareChat,
|
onCloseCreateShareChat,
|
||||||
refetchShareChatList,
|
refetchShareChatList,
|
||||||
resetShareChat,
|
resetShareChat,
|
||||||
@@ -136,7 +135,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
|||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>名称</Th>
|
<Th>名称</Th>
|
||||||
<Th>密码</Th>
|
|
||||||
<Th>最大上下文</Th>
|
<Th>最大上下文</Th>
|
||||||
<Th>tokens消耗</Th>
|
<Th>tokens消耗</Th>
|
||||||
<Th>最后使用时间</Th>
|
<Th>最后使用时间</Th>
|
||||||
@@ -147,7 +145,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
|||||||
{shareChatList.map((item) => (
|
{shareChatList.map((item) => (
|
||||||
<Tr key={item._id}>
|
<Tr key={item._id}>
|
||||||
<Td>{item.name}</Td>
|
<Td>{item.name}</Td>
|
||||||
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
|
|
||||||
<Td>{item.maxContext}</Td>
|
<Td>{item.maxContext}</Td>
|
||||||
<Td>{formatTokens(item.tokens)}</Td>
|
<Td>{formatTokens(item.tokens)}</Td>
|
||||||
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
||||||
@@ -160,7 +157,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
|||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
_hover={{ color: 'myBlue.600' }}
|
_hover={{ color: 'myBlue.600' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const url = `${location.origin}/chat/share?shareId=${item._id}`;
|
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||||
copyData(url, '已复制分享地址');
|
copyData(url, '已复制分享地址');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -216,17 +213,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</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}>
|
<FormControl mt={9}>
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
<Box flex={'0 0 120px'} w={0}>
|
<Box flex={'0 0 120px'} w={0}>
|
||||||
|
|||||||
@@ -1,66 +1,157 @@
|
|||||||
import { AppModuleItemType } from '@/types/app';
|
import { AppModuleItemType } from '@/types/app';
|
||||||
import { AppSchema } from '@/types/mongoSchema';
|
import { AppSchema } from '@/types/mongoSchema';
|
||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, {
|
||||||
import { Box, useOutsideClick, Flex, IconButton } from '@chakra-ui/react';
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
ForwardedRef
|
||||||
|
} from 'react';
|
||||||
|
import { Box, Flex, IconButton, useOutsideClick } from '@chakra-ui/react';
|
||||||
import MyIcon from '@/components/Icon';
|
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 = ({
|
export type ChatTestComponentRef = {
|
||||||
app,
|
resetChatTest: () => void;
|
||||||
modules,
|
};
|
||||||
onClose
|
|
||||||
}: {
|
|
||||||
app: AppSchema;
|
|
||||||
modules?: AppModuleItemType[];
|
|
||||||
onClose: () => void;
|
|
||||||
}) => {
|
|
||||||
const isOpen = useMemo(() => !!modules, [modules]);
|
|
||||||
|
|
||||||
const { ChatBox, ChatInput, ChatBoxParentRef, setChatHistory } = useChat({
|
const ChatTest = (
|
||||||
appId: app._id
|
{
|
||||||
|
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 (
|
useImperativeHandle(ref, () => ({
|
||||||
<Flex
|
resetChatTest() {
|
||||||
zIndex={3}
|
console.log(ChatBoxRef.current, '===');
|
||||||
flexDirection={'column'}
|
|
||||||
position={'absolute'}
|
|
||||||
top={5}
|
|
||||||
right={0}
|
|
||||||
h={isOpen ? '95%' : '0'}
|
|
||||||
w={isOpen ? '460px' : '0'}
|
|
||||||
bg={'white'}
|
|
||||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
|
||||||
borderRadius={'md'}
|
|
||||||
overflow={'hidden'}
|
|
||||||
transition={'.2s ease'}
|
|
||||||
>
|
|
||||||
<Flex py={4} px={5} whiteSpace={'nowrap'}>
|
|
||||||
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
|
||||||
调试预览
|
|
||||||
</Box>
|
|
||||||
<IconButton
|
|
||||||
className="chat"
|
|
||||||
size={'sm'}
|
|
||||||
icon={<MyIcon name={'clearLight'} w={'14px'} />}
|
|
||||||
variant={'base'}
|
|
||||||
borderRadius={'md'}
|
|
||||||
aria-label={'delete'}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setChatHistory([]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Box ref={ChatBoxParentRef} flex={1} px={5} overflow={'overlay'}>
|
|
||||||
<ChatBox appAvatar={app.avatar} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box px={5}>
|
ChatBoxRef.current?.resetHistory([]);
|
||||||
<ChatInput />
|
ChatBoxRef.current?.resetVariables();
|
||||||
</Box>
|
}
|
||||||
</Flex>
|
}));
|
||||||
|
|
||||||
|
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 }
|
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||||
}: NodeProps<FlowModuleItemType>) => {
|
}: NodeProps<FlowModuleItemType>) => {
|
||||||
return (
|
return (
|
||||||
<NodeCard
|
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
|
||||||
minW={'400px'}
|
|
||||||
logo={'/icon/logo.png'}
|
|
||||||
name={'知识库搜索'}
|
|
||||||
moduleId={moduleId}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Divider text="Input" />
|
<Divider text="Input" />
|
||||||
<Container>
|
<Container>
|
||||||
<RenderInput
|
<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;
|
onAddNode: (e: { template: AppModuleTemplateItemType; position: XYPosition }) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ContextMenuRef = useRef(null);
|
const BoxRef = useRef(null);
|
||||||
|
|
||||||
useOutsideClick({
|
useOutsideClick({
|
||||||
ref: ContextMenuRef,
|
ref: BoxRef,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ const ModuleStoreList = ({
|
|||||||
></Box>
|
></Box>
|
||||||
<Flex
|
<Flex
|
||||||
zIndex={3}
|
zIndex={3}
|
||||||
ref={ContextMenuRef}
|
ref={BoxRef}
|
||||||
flexDirection={'column'}
|
flexDirection={'column'}
|
||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
top={'65px'}
|
top={'65px'}
|
||||||
@@ -52,7 +52,7 @@ const ModuleStoreList = ({
|
|||||||
userSelect={'none'}
|
userSelect={'none'}
|
||||||
>
|
>
|
||||||
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||||
添加模块
|
系统模块
|
||||||
</Box>
|
</Box>
|
||||||
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||||
{ModuleTemplates.map((item) =>
|
{ModuleTemplates.map((item) =>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { Box, Flex, useTheme } from '@chakra-ui/react';
|
|||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import type { FlowModuleItemType } from '@/types/flow';
|
import type { FlowModuleItemType } from '@/types/flow';
|
||||||
|
import MyTooltip from '@/components/MyTooltip';
|
||||||
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode | React.ReactNode[] | string;
|
children: React.ReactNode | React.ReactNode[] | string;
|
||||||
logo?: string;
|
logo: string;
|
||||||
name?: string;
|
name: string;
|
||||||
intro?: string;
|
description?: string;
|
||||||
|
intro: string;
|
||||||
minW?: string | number;
|
minW?: string | number;
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
onDelNode: FlowModuleItemType['onDelNode'];
|
onDelNode: FlowModuleItemType['onDelNode'];
|
||||||
@@ -18,6 +21,7 @@ const NodeCard = ({
|
|||||||
children,
|
children,
|
||||||
logo = '/icon/logo.png',
|
logo = '/icon/logo.png',
|
||||||
name = '未知模块',
|
name = '未知模块',
|
||||||
|
description,
|
||||||
minW = '300px',
|
minW = '300px',
|
||||||
onDelNode,
|
onDelNode,
|
||||||
moduleId
|
moduleId
|
||||||
@@ -28,9 +32,15 @@ const NodeCard = ({
|
|||||||
<Box minW={minW} bg={'white'} border={theme.borders.md} borderRadius={'md'} boxShadow={'sm'}>
|
<Box minW={minW} bg={'white'} border={theme.borders.md} borderRadius={'md'} boxShadow={'sm'}>
|
||||||
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
|
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
|
||||||
<Avatar src={logo} borderRadius={'md'} w={'30px'} h={'30px'} />
|
<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}
|
{name}
|
||||||
</Box>
|
</Box>
|
||||||
|
{description && (
|
||||||
|
<MyTooltip label={description}>
|
||||||
|
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||||
|
</MyTooltip>
|
||||||
|
)}
|
||||||
|
<Box flex={1} />
|
||||||
<MyIcon
|
<MyIcon
|
||||||
className={'nodrag'}
|
className={'nodrag'}
|
||||||
name="delete"
|
name="delete"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import MySelect from '@/components/Select';
|
|||||||
import MySlider from '@/components/Slider';
|
import MySlider from '@/components/Slider';
|
||||||
import MyTooltip from '@/components/MyTooltip';
|
import MyTooltip from '@/components/MyTooltip';
|
||||||
|
|
||||||
const Label = ({
|
export const Label = ({
|
||||||
required = false,
|
required = false,
|
||||||
children,
|
children,
|
||||||
description
|
description
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import dynamic from 'next/dynamic';
|
|||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import ButtonEdge from './components/modules/ButtonEdge';
|
import ButtonEdge from './components/modules/ButtonEdge';
|
||||||
import MyTooltip from '@/components/MyTooltip';
|
import MyTooltip from '@/components/MyTooltip';
|
||||||
|
import TemplateList from './components/TemplateList';
|
||||||
|
import ChatTest, { type ChatTestComponentRef } from './components/ChatTest';
|
||||||
|
|
||||||
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
||||||
ssr: false
|
ssr: false
|
||||||
});
|
});
|
||||||
@@ -46,15 +49,12 @@ const NodeAnswer = dynamic(() => import('./components/NodeAnswer'), {
|
|||||||
const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput'), {
|
const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput'), {
|
||||||
ssr: false
|
ssr: false
|
||||||
});
|
});
|
||||||
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
const ChatTest = dynamic(() => import('./components/ChatTest'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
|
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
|
||||||
ssr: false
|
ssr: false
|
||||||
});
|
});
|
||||||
|
const NodeUserGuide = dynamic(() => import('./components/NodeUserGuide'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
@@ -63,6 +63,7 @@ import { AppModuleItemType, AppModuleTemplateItemType } from '@/types/app';
|
|||||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
|
[FlowModuleTypeEnum.userGuide]: NodeUserGuide,
|
||||||
[FlowModuleTypeEnum.questionInputNode]: NodeQuestionInput,
|
[FlowModuleTypeEnum.questionInputNode]: NodeQuestionInput,
|
||||||
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
||||||
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||||
@@ -78,6 +79,7 @@ type Props = { app: AppSchema; onBack: () => void };
|
|||||||
|
|
||||||
const AppEdit = ({ app, onBack }: Props) => {
|
const AppEdit = ({ app, onBack }: Props) => {
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
const ChatTestRef = useRef<ChatTestComponentRef>(null);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { x, y, zoom } = useViewport();
|
const { x, y, zoom } = useViewport();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||||
@@ -222,7 +224,10 @@ const AppEdit = ({ app, onBack }: Props) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
successToast: '保存配置成功',
|
successToast: '保存配置成功',
|
||||||
errorToast: '保存配置异常'
|
errorToast: '保存配置异常',
|
||||||
|
onSuccess() {
|
||||||
|
ChatTestRef.current?.resetChatTest();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const initData = useCallback(
|
const initData = useCallback(
|
||||||
@@ -289,8 +294,6 @@ const AppEdit = ({ app, onBack }: Props) => {
|
|||||||
aria-label={'save'}
|
aria-label={'save'}
|
||||||
variant={'base'}
|
variant={'base'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// @ts-ignore
|
|
||||||
onclickSave();
|
|
||||||
setTestModules(flow2Modules());
|
setTestModules(flow2Modules());
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -366,7 +369,12 @@ const AppEdit = ({ app, onBack }: Props) => {
|
|||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|
||||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
|
<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>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@@ -378,4 +386,4 @@ const Flow = (data: Props) => (
|
|||||||
</ReactFlowProvider>
|
</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 { useRouter } from 'next/router';
|
||||||
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
@@ -155,14 +155,14 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
{currentTab === TabEnum.settings && <Settings modelId={appId} />}
|
{currentTab === TabEnum.settings && <Settings appId={appId} />}
|
||||||
{currentTab === TabEnum.edit && (
|
{currentTab === TabEnum.edit && (
|
||||||
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||||
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{currentTab === TabEnum.API && <API modelId={appId} />}
|
{currentTab === TabEnum.API && <API appId={appId} />}
|
||||||
{currentTab === TabEnum.share && <Share modelId={appId} />}
|
{currentTab === TabEnum.share && <Share appId={appId} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
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 { useRouter } from 'next/router';
|
||||||
import { initShareChatInfo } from '@/api/chat';
|
import { initShareChatInfo } from '@/api/chat';
|
||||||
import type { ChatSiteItemType, ExportChatType } from '@/types/chat';
|
|
||||||
import {
|
import {
|
||||||
Textarea,
|
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuList,
|
|
||||||
MenuItem,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerOverlay,
|
DrawerOverlay,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
Card,
|
useTheme
|
||||||
useOutsideClick,
|
|
||||||
useTheme,
|
|
||||||
Input,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
|
|
||||||
import { streamFetch } from '@/api/fetch';
|
import { streamFetch } from '@/api/fetch';
|
||||||
import MyIcon from '@/components/Icon';
|
import { useShareChatStore, defaultHistory } from '@/store/shareChat';
|
||||||
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 SideBar from '@/components/SideBar';
|
import SideBar from '@/components/SideBar';
|
||||||
import Avatar from '@/components/Avatar';
|
import { gptMessage2ChatType } from '@/utils/adapt';
|
||||||
import Empty from './components/Empty';
|
import ChatHistorySlider from './components/ChatHistorySlider';
|
||||||
import { HUMAN_ICON } from '@/constants/chat';
|
import { getErrText } from '@/utils/tools';
|
||||||
import MyTooltip from '@/components/MyTooltip';
|
|
||||||
|
|
||||||
const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
|
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
|
||||||
loading: () => <Loading fixed={false} />,
|
import MyIcon from '@/components/Icon';
|
||||||
ssr: false
|
import Tag from '@/components/Tag';
|
||||||
});
|
import PageContainer from '@/components/PageContainer';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
const ShareChat = () => {
|
||||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
const theme = useTheme();
|
||||||
import { useChat } from '@/hooks/useChat';
|
|
||||||
|
|
||||||
const textareaMinH = '22px';
|
|
||||||
|
|
||||||
const Chat = () => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { shareId = '', historyId } = router.query as { shareId: string; historyId: string };
|
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 ChatBoxRef = useRef<ComponentRef>(null);
|
||||||
const PhoneContextShow = useRef(false);
|
|
||||||
|
|
||||||
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
|
||||||
// message messageContextMenuData
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
message: ChatSiteItemType;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
password,
|
|
||||||
setPassword,
|
|
||||||
shareChatHistory,
|
|
||||||
delShareHistoryById,
|
|
||||||
setShareChatHistory,
|
|
||||||
shareChatData,
|
shareChatData,
|
||||||
setShareChatData,
|
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,
|
shareChatHistory,
|
||||||
onClosePassword,
|
saveChatResponse,
|
||||||
historyId,
|
delShareChatHistoryItemById,
|
||||||
scrollToBottom,
|
delOneShareHistoryByHistoryId,
|
||||||
toast,
|
delManyShareChatHistoryByShareId
|
||||||
onOpenPassword,
|
} = useShareChatStore();
|
||||||
delShareChatHistory,
|
|
||||||
router
|
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], () => {
|
useQuery(['init', shareId, historyId], () => {
|
||||||
if (!shareId) {
|
return loadAppInfo(shareId);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!historyId) {
|
|
||||||
return router.replace(`/chat/share?shareId=${shareId}&historyId=${new Types.ObjectId()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadChatInfo();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}>
|
<PageContainer>
|
||||||
{/* pc always show history. */}
|
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}>
|
||||||
{isPc && (
|
{/* slider */}
|
||||||
<SideBar>
|
{isPc ? (
|
||||||
<ShareHistory
|
<SideBar>
|
||||||
onclickDelHistory={delShareHistoryById}
|
<ChatHistorySlider
|
||||||
onclickExportChat={onclickExportChat}
|
appName={shareChatData.app.name}
|
||||||
onCloseSlider={onCloseSlider}
|
appAvatar={shareChatData.app.avatar}
|
||||||
/>
|
activeHistoryId={historyId}
|
||||||
</SideBar>
|
history={shareChatHistory
|
||||||
)}
|
.filter((item) => item.shareId === shareId)
|
||||||
|
.map((item) => ({
|
||||||
{/* 聊天内容 */}
|
id: item._id,
|
||||||
<Flex
|
title: item.title
|
||||||
position={'relative'}
|
}))}
|
||||||
h={[0, '100%']}
|
onChangeChat={(historyId) => {
|
||||||
w={['100%', 0]}
|
router.push({
|
||||||
flex={'1 0 0'}
|
query: {
|
||||||
flexDirection={'column'}
|
historyId: historyId || '',
|
||||||
>
|
shareId
|
||||||
{/* chat header */}
|
}
|
||||||
<Flex
|
});
|
||||||
alignItems={'center'}
|
}}
|
||||||
justifyContent={'space-between'}
|
onDelHistory={delOneShareHistoryByHistoryId}
|
||||||
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}
|
|
||||||
onCloseSlider={onCloseSlider}
|
onCloseSlider={onCloseSlider}
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</SideBar>
|
||||||
</Drawer>
|
) : (
|
||||||
)}
|
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
|
||||||
{/* password input */}
|
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
|
||||||
{
|
<DrawerContent maxWidth={'250px'}>
|
||||||
<Modal isOpen={isOpenPassword} onClose={onClosePassword}>
|
<ChatHistorySlider
|
||||||
<ModalOverlay />
|
appName={shareChatData.app.name}
|
||||||
<ModalContent>
|
appAvatar={shareChatData.app.avatar}
|
||||||
<ModalCloseButton />
|
activeHistoryId={historyId}
|
||||||
<ModalHeader>安全密码</ModalHeader>
|
history={shareChatHistory.map((item) => ({
|
||||||
<ModalBody>
|
id: item._id,
|
||||||
<Flex alignItems={'center'}>
|
title: item.title
|
||||||
<Box flex={'0 0 70px'}>密码:</Box>
|
}))}
|
||||||
<Input
|
onChangeChat={(historyId) => {
|
||||||
type="password"
|
router.push({
|
||||||
autoFocus
|
query: {
|
||||||
placeholder="使用密码,无密码直接点确认"
|
historyId: historyId || '',
|
||||||
onBlur={(e) => setPassword(e.target.value)}
|
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>
|
</Flex>
|
||||||
<Button variant={'base'} mr={3} onClick={onClosePassword}>
|
{/* chat box */}
|
||||||
取消
|
<Box
|
||||||
</Button>
|
pt={[0, 5]}
|
||||||
<Button onClick={loadChatInfo}>确定</Button>
|
flex={1}
|
||||||
</ModalFooter>
|
maxW={['100%', '1000px', '1200px']}
|
||||||
</ModalContent>
|
px={[0, 5]}
|
||||||
</Modal>
|
w={'100%'}
|
||||||
}
|
mx={'auto'}
|
||||||
</Flex>
|
>
|
||||||
|
<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;
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { Schema, model, models, Model } from 'mongoose';
|
import { Schema, model, models, Model } from 'mongoose';
|
||||||
import { ShareChatSchema as ShareChatSchemaType } from '@/types/mongoSchema';
|
import { ShareChatSchema as ShareChatSchemaType } from '@/types/mongoSchema';
|
||||||
import { hashPassword } from '@/service/utils/tools';
|
|
||||||
|
|
||||||
const ShareChatSchema = new Schema({
|
const ShareChatSchema = new Schema({
|
||||||
|
shareId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'user',
|
ref: 'user',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
modelId: {
|
appId: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'model',
|
ref: 'model',
|
||||||
required: true
|
required: true
|
||||||
@@ -17,10 +20,6 @@ const ShareChatSchema = new Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
password: {
|
|
||||||
type: String,
|
|
||||||
set: (val: string) => hashPassword(val)
|
|
||||||
},
|
|
||||||
tokens: {
|
tokens: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
|||||||
@@ -317,30 +317,17 @@ export const authChat = async ({
|
|||||||
showModelDetail
|
showModelDetail
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const authShareChat = async ({
|
export const authShareChat = async ({ shareId }: { shareId: string }) => {
|
||||||
shareId,
|
|
||||||
password
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
password: string;
|
|
||||||
}) => {
|
|
||||||
// get shareChat
|
// get shareChat
|
||||||
const shareChat = await ShareChat.findById(shareId);
|
const shareChat = await ShareChat.findOne({ shareId });
|
||||||
|
|
||||||
if (!shareChat) {
|
if (!shareChat) {
|
||||||
return Promise.reject('分享链接已失效');
|
return Promise.reject('分享链接已失效');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shareChat.password !== hashPassword(password)) {
|
|
||||||
return Promise.reject({
|
|
||||||
code: 501,
|
|
||||||
message: '密码不正确'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: String(shareChat.userId),
|
userId: String(shareChat.userId),
|
||||||
appId: String(shareChat.modelId),
|
appId: String(shareChat.appId),
|
||||||
authType: 'token' as AuthType
|
authType: 'token' as AuthType
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ import { devtools, persist } from 'zustand/middleware';
|
|||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
import { OpenAiChatEnum } from '@/constants/model';
|
import { OpenAiChatEnum } from '@/constants/model';
|
||||||
|
|
||||||
import {
|
import { ChatSiteItemType, HistoryItemType, ChatType } from '@/types/chat';
|
||||||
ChatSiteItemType,
|
|
||||||
HistoryItemType,
|
|
||||||
ShareChatHistoryItemType,
|
|
||||||
ChatType,
|
|
||||||
ShareChatType
|
|
||||||
} from '@/types/chat';
|
|
||||||
import { getChatHistory } from '@/api/chat';
|
import { getChatHistory } from '@/api/chat';
|
||||||
import { HUMAN_ICON } from '@/constants/chat';
|
import { HUMAN_ICON } from '@/constants/chat';
|
||||||
|
|
||||||
@@ -32,16 +26,6 @@ type State = {
|
|||||||
setLastChatModelId: (id: string) => void;
|
setLastChatModelId: (id: string) => void;
|
||||||
lastChatId: string;
|
lastChatId: string;
|
||||||
setLastChatId: (id: string) => void;
|
setLastChatId: (id: string) => void;
|
||||||
|
|
||||||
shareChatData: ShareChatType;
|
|
||||||
setShareChatData: (e?: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void;
|
|
||||||
password: string;
|
|
||||||
setPassword: (val: string) => void;
|
|
||||||
shareChatHistory: ShareChatHistoryItemType[];
|
|
||||||
setShareChatHistory: (e: SetShareChatHistoryItem) => void;
|
|
||||||
delShareHistoryById: (historyId: string) => void;
|
|
||||||
delShareChatHistoryItemById: (historyId: string, index: number) => void;
|
|
||||||
delShareChatHistory: (shareId?: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultChatData: ChatType = {
|
const defaultChatData: ChatType = {
|
||||||
@@ -56,18 +40,6 @@ const defaultChatData: ChatType = {
|
|||||||
chatModel: OpenAiChatEnum.GPT3516k,
|
chatModel: OpenAiChatEnum.GPT3516k,
|
||||||
history: []
|
history: []
|
||||||
};
|
};
|
||||||
const defaultShareChatData: ShareChatType = {
|
|
||||||
appId: '',
|
|
||||||
maxContext: 5,
|
|
||||||
userAvatar: HUMAN_ICON,
|
|
||||||
model: {
|
|
||||||
name: '',
|
|
||||||
avatar: '/icon/logo.png',
|
|
||||||
intro: ''
|
|
||||||
},
|
|
||||||
chatModel: OpenAiChatEnum.GPT3516k,
|
|
||||||
history: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useChatStore = create<State>()(
|
export const useChatStore = create<State>()(
|
||||||
devtools(
|
devtools(
|
||||||
@@ -114,114 +86,13 @@ export const useChatStore = create<State>()(
|
|||||||
state.chatData = e;
|
state.chatData = e;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
shareChatData: defaultShareChatData,
|
|
||||||
setShareChatData(
|
|
||||||
e: ShareChatType | ((e: ShareChatType) => ShareChatType) = defaultShareChatData
|
|
||||||
) {
|
|
||||||
if (typeof e === 'function') {
|
|
||||||
set((state) => {
|
|
||||||
state.shareChatData = e(state.shareChatData);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set((state) => {
|
|
||||||
state.shareChatData = e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
password: '',
|
|
||||||
setPassword(val: string) {
|
|
||||||
set((state) => {
|
|
||||||
state.password = val;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
shareChatHistory: [],
|
|
||||||
setShareChatHistory({
|
|
||||||
historyId,
|
|
||||||
shareId,
|
|
||||||
title,
|
|
||||||
latestChat,
|
|
||||||
chats = []
|
|
||||||
}: SetShareChatHistoryItem) {
|
|
||||||
set((state) => {
|
|
||||||
const history = state.shareChatHistory.find((item) => item._id === historyId);
|
|
||||||
let historyList: ShareChatHistoryItemType[] = [];
|
|
||||||
if (history) {
|
|
||||||
historyList = state.shareChatHistory.map((item) =>
|
|
||||||
item._id === historyId
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
title,
|
|
||||||
latestChat,
|
|
||||||
updateTime: new Date(),
|
|
||||||
chats
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
historyList = [
|
|
||||||
...state.shareChatHistory,
|
|
||||||
{
|
|
||||||
_id: historyId,
|
|
||||||
shareId,
|
|
||||||
title,
|
|
||||||
latestChat,
|
|
||||||
updateTime: new Date(),
|
|
||||||
chats
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
historyList.sort((a, b) => new Date(b.updateTime) - new Date(a.updateTime));
|
|
||||||
|
|
||||||
state.shareChatHistory = historyList.slice(0, 30);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
delShareHistoryById(historyId: string) {
|
|
||||||
set((state) => {
|
|
||||||
state.shareChatHistory = state.shareChatHistory.filter(
|
|
||||||
(item) => item._id !== historyId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
delShareChatHistoryItemById(historyId: string, index: number) {
|
|
||||||
set((state) => {
|
|
||||||
// update history store
|
|
||||||
const newHistoryList = state.shareChatHistory.map((item) =>
|
|
||||||
item._id === historyId
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
chats: [...item.chats.slice(0, index), ...item.chats.slice(index + 1)]
|
|
||||||
}
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
state.shareChatHistory = newHistoryList;
|
|
||||||
|
|
||||||
// update chatData
|
|
||||||
state.shareChatData.history =
|
|
||||||
newHistoryList.find((item) => item._id === historyId)?.chats || [];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
delShareChatHistory(shareId?: string) {
|
|
||||||
set((state) => {
|
|
||||||
if (shareId) {
|
|
||||||
state.shareChatHistory = state.shareChatHistory.filter(
|
|
||||||
(item) => item.shareId !== shareId
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
state.shareChatHistory = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: 'chatStore',
|
name: 'chatStore',
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
lastChatModelId: state.lastChatModelId,
|
lastChatModelId: state.lastChatModelId,
|
||||||
lastChatId: state.lastChatId,
|
lastChatId: state.lastChatId
|
||||||
password: state.password,
|
|
||||||
shareChatHistory: state.shareChatHistory
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
146
client/src/store/shareChat.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
import { immer } from 'zustand/middleware/immer';
|
||||||
|
|
||||||
|
import type { ChatSiteItemType, ShareChatHistoryItemType, ShareChatType } from '@/types/chat';
|
||||||
|
import { HUMAN_ICON } from '@/constants/chat';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
shareChatData: ShareChatType;
|
||||||
|
setShareChatData: (e: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void;
|
||||||
|
shareChatHistory: ShareChatHistoryItemType[];
|
||||||
|
saveChatResponse: (e: {
|
||||||
|
historyId: string;
|
||||||
|
prompts: ChatSiteItemType[];
|
||||||
|
variables: Record<string, any>;
|
||||||
|
shareId: string;
|
||||||
|
}) => { newChatId: string };
|
||||||
|
delOneShareHistoryByHistoryId: (historyId: string) => void;
|
||||||
|
delShareChatHistoryItemById: (e: { historyId: string; index: number }) => void;
|
||||||
|
delManyShareChatHistoryByShareId: (shareId?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultHistory: ShareChatHistoryItemType = {
|
||||||
|
_id: `${Date.now()}`,
|
||||||
|
updateTime: new Date(),
|
||||||
|
title: '新对话',
|
||||||
|
shareId: '',
|
||||||
|
chats: []
|
||||||
|
};
|
||||||
|
const defaultShareChatData: ShareChatType = {
|
||||||
|
maxContext: 5,
|
||||||
|
userAvatar: HUMAN_ICON,
|
||||||
|
app: {
|
||||||
|
name: '',
|
||||||
|
avatar: '/icon/logo.png',
|
||||||
|
intro: ''
|
||||||
|
},
|
||||||
|
history: defaultHistory
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShareChatStore = create<State>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
immer((set, get) => ({
|
||||||
|
shareChatData: defaultShareChatData,
|
||||||
|
setShareChatData(e) {
|
||||||
|
const val = (() => {
|
||||||
|
if (typeof e === 'function') {
|
||||||
|
return e(get().shareChatData);
|
||||||
|
} else {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
set((state) => {
|
||||||
|
state.shareChatData = val;
|
||||||
|
// update history
|
||||||
|
state.shareChatHistory = state.shareChatHistory.map((item) =>
|
||||||
|
item._id === val.history._id ? val.history : item
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
shareChatHistory: [],
|
||||||
|
saveChatResponse({ historyId, prompts, variables, shareId }) {
|
||||||
|
const history = get().shareChatHistory.find((item) => item._id === historyId);
|
||||||
|
|
||||||
|
const newChatId = history ? '' : nanoid();
|
||||||
|
|
||||||
|
const historyList = (() => {
|
||||||
|
if (history) {
|
||||||
|
return get().shareChatHistory.map((item) =>
|
||||||
|
item._id === historyId
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
title: prompts[prompts.length - 2]?.value,
|
||||||
|
updateTime: new Date(),
|
||||||
|
chats: prompts,
|
||||||
|
variables
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return get().shareChatHistory.concat({
|
||||||
|
_id: newChatId,
|
||||||
|
shareId,
|
||||||
|
title: prompts[prompts.length - 2]?.value,
|
||||||
|
updateTime: new Date(),
|
||||||
|
chats: prompts,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
historyList.sort((a, b) => new Date(b.updateTime) - new Date(a.updateTime));
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
state.shareChatHistory = historyList.slice(0, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
newChatId
|
||||||
|
};
|
||||||
|
},
|
||||||
|
delOneShareHistoryByHistoryId(historyId: string) {
|
||||||
|
set((state) => {
|
||||||
|
state.shareChatHistory = state.shareChatHistory.filter(
|
||||||
|
(item) => item._id !== historyId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delShareChatHistoryItemById({ historyId, index }) {
|
||||||
|
set((state) => {
|
||||||
|
// update history store
|
||||||
|
const newHistoryList = state.shareChatHistory.map((item) =>
|
||||||
|
item._id === historyId
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
chats: [...item.chats.slice(0, index), ...item.chats.slice(index + 1)]
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
state.shareChatHistory = newHistoryList;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delManyShareChatHistoryByShareId(shareId?: string) {
|
||||||
|
set((state) => {
|
||||||
|
if (shareId) {
|
||||||
|
state.shareChatHistory = state.shareChatHistory.filter(
|
||||||
|
(item) => item.shareId !== shareId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
state.shareChatHistory = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: 'shareChatStore',
|
||||||
|
partialize: (state) => ({
|
||||||
|
shareChatHistory: state.shareChatHistory
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
24
client/src/types/app.d.ts
vendored
@@ -1,6 +1,10 @@
|
|||||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||||
import { XYPosition } from 'reactflow';
|
import { XYPosition } from 'reactflow';
|
||||||
import { AppModuleItemTypeEnum, ModulesInputItemTypeEnum } from '../constants/app';
|
import {
|
||||||
|
AppModuleItemTypeEnum,
|
||||||
|
ModulesInputItemTypeEnum,
|
||||||
|
VariableInputEnum
|
||||||
|
} from '../constants/app';
|
||||||
import type { FlowInputItemType, FlowOutputItemType } from './flow';
|
import type { FlowInputItemType, FlowOutputItemType } from './flow';
|
||||||
import type { AppSchema, kbSchema } from './mongoSchema';
|
import type { AppSchema, kbSchema } from './mongoSchema';
|
||||||
import { ChatModelType } from '@/constants/model';
|
import { ChatModelType } from '@/constants/model';
|
||||||
@@ -33,7 +37,6 @@ export interface ShareAppItem {
|
|||||||
|
|
||||||
export type ShareChatEditType = {
|
export type ShareChatEditType = {
|
||||||
name: string;
|
name: string;
|
||||||
password: string;
|
|
||||||
maxContext: number;
|
maxContext: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,11 +47,22 @@ export type ClassifyQuestionAgentItemType = {
|
|||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VariableItemType = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: `${VariableInputEnum}`;
|
||||||
|
required: boolean;
|
||||||
|
maxLen: number;
|
||||||
|
enums: { value: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
/* app module */
|
/* app module */
|
||||||
export type AppModuleTemplateItemType = {
|
export type AppModuleTemplateItemType = {
|
||||||
logo?: string;
|
logo: string;
|
||||||
name?: string;
|
name: string;
|
||||||
intro?: string;
|
description?: string;
|
||||||
|
intro: string;
|
||||||
|
|
||||||
flowType: `${FlowModuleTypeEnum}`;
|
flowType: `${FlowModuleTypeEnum}`;
|
||||||
type: `${AppModuleItemTypeEnum}`;
|
type: `${AppModuleItemTypeEnum}`;
|
||||||
|
|||||||
20
client/src/types/chat.d.ts
vendored
@@ -22,24 +22,22 @@ export interface ChatType extends InitChatResponse {
|
|||||||
history: ChatSiteItemType[];
|
history: ChatSiteItemType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShareChatType extends InitShareChatResponse {
|
|
||||||
history: ChatSiteItemType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HistoryItemType = {
|
export type HistoryItemType = {
|
||||||
_id: string;
|
_id: string;
|
||||||
updateTime: Date;
|
updateTime: Date;
|
||||||
modelId: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
latestChat: string;
|
};
|
||||||
|
export type ChatHistoryItemType = HistoryItemType & {
|
||||||
|
appId: string;
|
||||||
top: boolean;
|
top: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShareChatHistoryItemType = {
|
export type ShareChatHistoryItemType = HistoryItemType & {
|
||||||
_id: string;
|
|
||||||
shareId: string;
|
shareId: string;
|
||||||
updateTime: Date;
|
variables?: Record<string, any>;
|
||||||
title: string;
|
|
||||||
latestChat: string;
|
|
||||||
chats: ChatSiteItemType[];
|
chats: ChatSiteItemType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShareChatType = InitShareChatResponse & {
|
||||||
|
history: ShareChatHistoryItemType;
|
||||||
|
};
|
||||||
|
|||||||
4
client/src/types/mongoSchema.d.ts
vendored
@@ -132,9 +132,9 @@ export interface PromotionRecordSchema {
|
|||||||
|
|
||||||
export interface ShareChatSchema {
|
export interface ShareChatSchema {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
shareId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
modelId: string;
|
appId: string;
|
||||||
password: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
tokens: number;
|
tokens: number;
|
||||||
maxContext: number;
|
maxContext: number;
|
||||||
|
|||||||