Commit b2e2f60e by archer

chatbox ui

parent eb768d9c
import { GET, POST, DELETE, PUT } from './request'; import { GET, POST, DELETE, PUT } from './request';
import type { AppSchema } from '@/types/mongoSchema'; import type { AppSchema } from '@/types/mongoSchema';
import type { AppModuleItemType, AppUpdateParams } from '@/types/app'; import type { AppListItemType, AppUpdateParams } from '@/types/app';
import { RequestPaging } from '../types/index'; import { RequestPaging } from '../types/index';
import type { AppListResponse } from './response/app';
import type { Props as CreateAppProps } from '@/pages/api/app/create'; import type { Props as CreateAppProps } from '@/pages/api/app/create';
/** /**
* 获取模型列表 * 获取模型列表
*/ */
export const getMyModels = () => GET<AppListResponse>('/app/list'); export const getMyModels = () => GET<AppListItemType[]>('/app/myApps');
/** /**
* 创建一个模型 * 创建一个模型
...@@ -18,12 +17,12 @@ export const postCreateApp = (data: CreateAppProps) => POST<string>('/app/create ...@@ -18,12 +17,12 @@ export const postCreateApp = (data: CreateAppProps) => POST<string>('/app/create
/** /**
* 根据 ID 删除模型 * 根据 ID 删除模型
*/ */
export const delModelById = (id: string) => DELETE(`/app/del?modelId=${id}`); export const delModelById = (id: string) => DELETE(`/app/del?appId=${id}`);
/** /**
* 根据 ID 获取模型 * 根据 ID 获取模型
*/ */
export const getModelById = (id: string) => GET<AppSchema>(`/app/detail?modelId=${id}`); export const getModelById = (id: string) => GET<AppSchema>(`/app/detail?appId=${id}`);
/** /**
* 根据 ID 更新模型 * 根据 ID 更新模型
...@@ -41,5 +40,5 @@ export const getShareModelList = (data: { searchText?: string } & RequestPaging) ...@@ -41,5 +40,5 @@ export const getShareModelList = (data: { searchText?: string } & RequestPaging)
/** /**
* 收藏/取消收藏模型 * 收藏/取消收藏模型
*/ */
export const triggerModelCollection = (modelId: string) => export const triggerModelCollection = (appId: string) =>
POST<number>(`/app/share/collection?modelId=${modelId}`); POST<number>(`/app/share/collection?appId=${appId}`);
import { GET, POST, DELETE, PUT } from './request'; import { GET, POST, DELETE, PUT } from './request';
import type { HistoryItemType } from '@/types/chat'; import type { ChatHistoryItemType } from '@/types/chat';
import type { InitChatResponse, InitShareChatResponse } from './response/chat'; import type { InitChatResponse, InitShareChatResponse } from './response/chat';
import { RequestPaging } from '../types/index'; import { RequestPaging } from '../types/index';
import type { ShareChatSchema } from '@/types/mongoSchema'; import type { ShareChatSchema } from '@/types/mongoSchema';
...@@ -11,14 +11,14 @@ import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updat ...@@ -11,14 +11,14 @@ import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updat
/** /**
* 获取初始化聊天内容 * 获取初始化聊天内容
*/ */
export const getInitChatSiteInfo = (modelId: '' | string, chatId: '' | string) => export const getInitChatSiteInfo = (data: { appId: string; historyId?: string }) =>
GET<InitChatResponse>(`/chat/init?modelId=${modelId}&chatId=${chatId}`); GET<InitChatResponse>(`/chat/init`, data);
/** /**
* 获取历史记录 * 获取历史记录
*/ */
export const getChatHistory = (data: RequestPaging) => export const getChatHistory = (data: RequestPaging & { appId?: string }) =>
POST<HistoryItemType[]>('/chat/history/getHistory', data); POST<ChatHistoryItemType[]>('/chat/history/getHistory', data);
/** /**
* 删除一条历史记录 * 删除一条历史记录
...@@ -44,8 +44,8 @@ export const updateHistoryQuote = (params: { ...@@ -44,8 +44,8 @@ export const updateHistoryQuote = (params: {
/** /**
* 删除一句对话 * 删除一句对话
*/ */
export const delChatRecordByIndex = (chatId: string, contentId: string) => export const delChatRecordByIndex = (data: { historyId: string; contentId: string }) =>
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`); DELETE(`/chat/delChatRecordByContentId`, data);
/** /**
* 修改历史记录: 标题/置顶 * 修改历史记录: 标题/置顶
......
...@@ -9,12 +9,12 @@ interface StreamFetchProps { ...@@ -9,12 +9,12 @@ interface StreamFetchProps {
abortSignal: AbortController; abortSignal: AbortController;
} }
export const streamFetch = ({ export const streamFetch = ({
url = '/api/openapi/v1/chat/completions2', url = '/api/openapi/v1/chat/completions',
data, data,
onMessage, onMessage,
abortSignal abortSignal
}: StreamFetchProps) => }: StreamFetchProps) =>
new Promise<{ responseText: string; errMsg: string; newChatId: string | null }>( new Promise<{ responseText: string; errMsg: string; newHistoryId: string | null }>(
async (resolve, reject) => { async (resolve, reject) => {
try { try {
const response = await window.fetch(url, { const response = await window.fetch(url, {
...@@ -43,7 +43,7 @@ export const streamFetch = ({ ...@@ -43,7 +43,7 @@ export const streamFetch = ({
// response data // response data
let responseText = ''; let responseText = '';
let errMsg = ''; let errMsg = '';
const newChatId = response.headers.get('newChatId'); const newHistoryId = response.headers.get('newHistoryId');
const read = async () => { const read = async () => {
try { try {
...@@ -53,7 +53,7 @@ export const streamFetch = ({ ...@@ -53,7 +53,7 @@ export const streamFetch = ({
return resolve({ return resolve({
responseText, responseText,
errMsg, errMsg,
newChatId newHistoryId
}); });
} else { } else {
return reject('响应过程出现异常~'); return reject('响应过程出现异常~');
...@@ -85,7 +85,7 @@ export const streamFetch = ({ ...@@ -85,7 +85,7 @@ export const streamFetch = ({
return resolve({ return resolve({
responseText, responseText,
errMsg, errMsg,
newChatId newHistoryId
}); });
} }
reject(getErrText(err, '请求异常')); reject(getErrText(err, '请求异常'));
......
...@@ -92,8 +92,8 @@ function request(url: string, data: any, config: ConfigType, method: Method): an ...@@ -92,8 +92,8 @@ function request(url: string, data: any, config: ConfigType, method: Method): an
baseURL: '/api', baseURL: '/api',
url, url,
method, method,
data: method === 'GET' ? null : data, data: ['POST', 'PUT'].includes(method) ? data : null,
params: method === 'GET' ? data : null, // get请求不携带data,params放在url上 params: !['POST', 'PUT'].includes(method) ? data : null,
...config // 用户自定义配置,可以覆盖前面的配置 ...config // 用户自定义配置,可以覆盖前面的配置
}) })
.then((res) => checkRes(res.data)) .then((res) => checkRes(res.data))
...@@ -119,6 +119,6 @@ export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise ...@@ -119,6 +119,6 @@ export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise
return request(url, data, config, 'PUT'); return request(url, data, config, 'PUT');
} }
export function DELETE<T>(url: string, config: ConfigType = {}): Promise<T> { export function DELETE<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, {}, config, 'DELETE'); return request(url, data, config, 'DELETE');
} }
import type { ChatPopulate, AppSchema } from '@/types/mongoSchema'; import type { AppSchema } from '@/types/mongoSchema';
import type { ChatItemType } from '@/types/chat'; import type { ChatItemType } from '@/types/chat';
import { VariableItemType } from '@/types/app'; import { VariableItemType } from '@/types/app';
export interface InitChatResponse { export interface InitChatResponse {
chatId: string; historyId: string;
modelId: string; appId: string;
systemPrompt?: string; app: {
limitPrompt?: string; variableModules?: VariableItemType[];
model: { welcomeText?: string;
name: string; name: string;
avatar: string; avatar: string;
intro: string; intro: string;
canUse: boolean; canUse: boolean;
}; };
chatModel: AppSchema['chat']['chatModel']; // 对话模型名 title: string;
variables: Record<string, any>;
history: ChatItemType[]; history: ChatItemType[];
} }
......
...@@ -38,8 +38,10 @@ export type StartChatFnProps = { ...@@ -38,8 +38,10 @@ export type StartChatFnProps = {
}; };
export type ComponentRef = { export type ComponentRef = {
getChatHistory: () => ChatSiteItemType[];
resetVariables: (data?: Record<string, any>) => void; resetVariables: (data?: Record<string, any>) => void;
resetHistory: (history: ChatSiteItemType[]) => void; resetHistory: (history: ChatSiteItemType[]) => void;
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
}; };
const VariableLabel = ({ const VariableLabel = ({
...@@ -73,7 +75,7 @@ const ChatBox = ( ...@@ -73,7 +75,7 @@ const ChatBox = (
welcomeText?: string; welcomeText?: string;
onUpdateVariable?: (e: Record<string, any>) => void; onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat: (e: StartChatFnProps) => Promise<{ responseText: string }>; onStartChat: (e: StartChatFnProps) => Promise<{ responseText: string }>;
onDelMessage?: (e: { id?: string; index: number }) => void; onDelMessage?: (e: { contentId?: string; index: number }) => void;
}, },
ref: ForwardedRef<ComponentRef> ref: ForwardedRef<ComponentRef>
) => { ) => {
...@@ -279,6 +281,7 @@ const ChatBox = ( ...@@ -279,6 +281,7 @@ const ChatBox = (
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getChatHistory: () => chatHistory,
resetVariables(e) { resetVariables(e) {
const defaultVal: Record<string, any> = {}; const defaultVal: Record<string, any> = {};
variableModules?.forEach((item) => { variableModules?.forEach((item) => {
...@@ -290,7 +293,8 @@ const ChatBox = ( ...@@ -290,7 +293,8 @@ const ChatBox = (
}, },
resetHistory(e) { resetHistory(e) {
setChatHistory(e); setChatHistory(e);
} },
scrollToBottom
})); }));
const controlIconStyle = { const controlIconStyle = {
...@@ -305,210 +309,215 @@ const ChatBox = ( ...@@ -305,210 +309,215 @@ const ChatBox = (
}; };
const controlContainerStyle = { const controlContainerStyle = {
className: 'control', className: 'control',
display: ['flex', 'none'], display: isChatting ? 'none' : ['flex', 'none'],
color: 'myGray.400', color: 'myGray.400',
pl: 1, pl: 1,
mt: 2, mt: 2,
position: 'absolute' as any, position: 'absolute' as any,
zIndex: 1 zIndex: 1,
w: '100%'
}; };
return ( return (
<Flex flexDirection={'column'} h={'100%'}> <Flex flexDirection={'column'} h={'100%'}>
<Box ref={ChatBoxRef} flex={'1 0 0'} overflow={'overlay'} px={[2, 5]}> <Box ref={ChatBoxRef} flex={'1 0 0'} overflow={'overlay'} px={[2, 5, 8]} py={5}>
{/* variable input */} <Box maxW={['100%', '1000px', '1200px']} mx={'auto'}>
{(variableModules || welcomeText) && ( {/* variable input */}
<Flex alignItems={'flex-start'} py={2}> {(variableModules || welcomeText) && (
{/* avatar */} <Flex alignItems={'flex-start'} py={2}>
<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 */}
<Avatar <Avatar
src={item.obj === 'Human' ? userInfo?.avatar || HUMAN_ICON : appAvatar} src={appAvatar}
w={isLargeWidth ? '34px' : '24px'} w={['24px', '34px']}
h={isLargeWidth ? '34px' : '24px'} h={['24px', '34px']}
{...(item.obj === 'AI' order={1}
? { mr={['6px', 2]}
order: 1,
mr: ['6px', 2]
}
: {
order: 3,
ml: ['6px', 2]
})}
/> />
{/* message */} {/* message */}
<Box order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}> <Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
{item.obj === 'AI' ? ( <Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
<Box w={'100%'}> {welcomeText && (
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}> <Box mb={2} pb={2} borderBottom={theme.borders.base}>
<Markdown {welcomeText}
source={item.value} </Box>
isChatting={index === chatHistory.length - 1 && isChatting} )}
/> {variableModules && (
</Card> <Box>
<Flex {...controlContainerStyle}> {variableModules.map((item) => (
<MyTooltip label={'复制'}> <Box w={'min(100%,300px)'} key={item.id} mb={4}>
<MyIcon <VariableLabel required={item.required}>{item.label}</VariableLabel>
{...controlIconStyle} {item.type === VariableInputEnum.input && (
name={'copy'} <Input
_hover={{ color: 'myBlue.700' }} isDisabled={variableIsFinish}
onClick={() => onclickCopy(item.value)} {...register(item.key, {
/> required: item.required
</MyTooltip> })}
{onDelMessage && ( />
<MyTooltip label={'删除'}> )}
<MyIcon {item.type === VariableInputEnum.select && (
{...controlIconStyle} <MySelect
name={'delete'} width={'100%'}
_hover={{ color: 'red.600' }} isDisabled={variableIsFinish}
onClick={() => { list={(item.enums || []).map((item) => ({
setChatHistory((state) => label: item.value,
state.filter((chat) => chat._id !== item._id) value: item.value
); }))}
onDelMessage({ {...register(item.key, {
id: item._id, required: item.required
index })}
}); onchange={(e) => {
}} setValue(item.key, e);
/> // setRefresh((state) => !state);
</MyTooltip> }}
/>
)}
</Box>
))}
{!variableIsFinish && (
<Button
leftIcon={<MyIcon name={'chatFill'} w={'16px'} />}
size={'sm'}
maxW={'100px'}
borderRadius={'lg'}
onClick={handleSubmit((data) => {
onUpdateVariable?.(data);
setVariables(data);
})}
>
{'开始对话'}
</Button>
)} )}
{hasVoiceApi && ( </Box>
<MyTooltip label={'语音播报'}> )}
</Card>
</Flex>
</Flex>
)}
{/* chat history */}
<Box id={'history'}>
{chatHistory.map((item, index) => (
<Flex
key={item._id}
alignItems={'flex-start'}
py={4}
_hover={{
'& .control': {
display: 'flex'
}
}}
>
{item.obj === 'Human' && <Box flex={1} />}
{/* avatar */}
<Avatar
src={item.obj === 'Human' ? userInfo?.avatar || HUMAN_ICON : appAvatar}
w={['24px', '34px']}
h={['24px', '34px']}
{...(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%'} position={'relative'}>
<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 <MyIcon
{...controlIconStyle} {...controlIconStyle}
name={'voice'} name={'copy'}
_hover={{ color: '#E74694' }} _hover={{ color: 'myBlue.700' }}
onClick={() => voiceBroadcast({ text: item.value })} onClick={() => onclickCopy(item.value)}
/> />
</MyTooltip> </MyTooltip>
)} {onDelMessage && (
</Flex> <MyTooltip label={'删除'}>
</Box> <MyIcon
) : ( {...controlIconStyle}
<Box position={'relative'}> name={'delete'}
<Card _hover={{ color: 'red.600' }}
className="markdown" onClick={() => {
whiteSpace={'pre-wrap'} setChatHistory((state) =>
px={4} state.filter((chat) => chat._id !== item._id)
py={3} );
borderRadius={'8px 0 8px 8px'} onDelMessage({
bg={'myBlue.300'} contentId: item._id,
> index
<Box as={'p'}>{item.value}</Box> });
</Card> }}
<Flex {...controlContainerStyle} right={0}> />
<MyTooltip label={'复制'}> </MyTooltip>
<MyIcon )}
{...controlIconStyle} {hasVoiceApi && (
name={'copy'} <MyTooltip label={'语音播报'}>
_hover={{ color: 'myBlue.700' }} <MyIcon
onClick={() => onclickCopy(item.value)} {...controlIconStyle}
/> name={'voice'}
</MyTooltip> _hover={{ color: '#E74694' }}
{onDelMessage && ( onClick={() => voiceBroadcast({ text: item.value })}
<MyTooltip label={'删除'}> />
</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 <MyIcon
{...controlIconStyle} {...controlIconStyle}
mr={0} name={'copy'}
name={'delete'} _hover={{ color: 'myBlue.700' }}
_hover={{ color: 'red.600' }} onClick={() => onclickCopy(item.value)}
onClick={() => {
setChatHistory((state) =>
state.filter((chat) => chat._id !== item._id)
);
onDelMessage({
id: item._id,
index
});
}}
/> />
</MyTooltip> </MyTooltip>
)} {onDelMessage && (
</Flex> <MyTooltip label={'删除'}>
</Box> <MyIcon
)} {...controlIconStyle}
</Box> mr={0}
</Flex> name={'delete'}
))} _hover={{ color: 'red.600' }}
onClick={() => {
setChatHistory((state) =>
state.filter((chat) => chat._id !== item._id)
);
onDelMessage({
contentId: item._id,
index
});
}}
/>
</MyTooltip>
)}
</Flex>
</Box>
)}
</Box>
</Flex>
))}
</Box>
</Box> </Box>
</Box> </Box>
{variableIsFinish ? ( {variableIsFinish ? (
...@@ -531,7 +540,6 @@ const ChatBox = ( ...@@ -531,7 +540,6 @@ const ChatBox = (
_focusVisible={{ _focusVisible={{
border: 'none' border: 'none'
}} }}
isDisabled={isChatting}
placeholder="提问" placeholder="提问"
resize={'none'} resize={'none'}
rows={1} rows={1}
......
...@@ -15,7 +15,8 @@ const pcUnShowLayoutRoute: Record<string, boolean> = { ...@@ -15,7 +15,8 @@ const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true, '/': true,
'/login': true, '/login': true,
'/chat/share': true, '/chat/share': true,
'/app/edit': true '/app/edit': true,
'/chat': true
}; };
const phoneUnShowLayoutRoute: Record<string, boolean> = { const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/': true, '/': true,
......
...@@ -17,14 +17,14 @@ export enum NavbarTypeEnum { ...@@ -17,14 +17,14 @@ export enum NavbarTypeEnum {
const Navbar = ({ unread }: { unread: number }) => { const Navbar = ({ unread }: { unread: number }) => {
const router = useRouter(); const router = useRouter();
const { userInfo, lastModelId } = useUserStore(); const { userInfo, lastModelId } = useUserStore();
const { lastChatModelId, lastChatId } = useChatStore(); const { lastChatAppId, lastChatId } = useChatStore();
const navbarList = useMemo( const navbarList = useMemo(
() => [ () => [
{ {
label: '聊天', label: '聊天',
icon: 'chatLight', icon: 'chatLight',
activeIcon: 'chatFill', activeIcon: 'chatFill',
link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`, link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`,
activeLink: ['/chat'] activeLink: ['/chat']
}, },
{ {
...@@ -56,7 +56,7 @@ const Navbar = ({ unread }: { unread: number }) => { ...@@ -56,7 +56,7 @@ const Navbar = ({ unread }: { unread: number }) => {
activeLink: ['/number'] activeLink: ['/number']
} }
], ],
[lastChatId, lastChatModelId] [lastChatId, lastChatAppId]
); );
const itemStyles: any = { const itemStyles: any = {
......
...@@ -7,13 +7,13 @@ import Badge from '../Badge'; ...@@ -7,13 +7,13 @@ import Badge from '../Badge';
const NavbarPhone = ({ unread }: { unread: number }) => { const NavbarPhone = ({ unread }: { unread: number }) => {
const router = useRouter(); const router = useRouter();
const { lastChatModelId, lastChatId } = useChatStore(); const { lastChatAppId, lastChatId } = useChatStore();
const navbarList = useMemo( const navbarList = useMemo(
() => [ () => [
{ {
label: '聊天', label: '聊天',
icon: 'tabbarChat', icon: 'tabbarChat',
link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`, link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`,
activeLink: ['/chat'], activeLink: ['/chat'],
unread: 0 unread: 0
}, },
...@@ -39,7 +39,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => { ...@@ -39,7 +39,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
unread unread
} }
], ],
[lastChatId, lastChatModelId, unread] [lastChatId, lastChatAppId, unread]
); );
return ( return (
......
...@@ -7,7 +7,7 @@ interface Props extends BoxProps {} ...@@ -7,7 +7,7 @@ interface Props extends BoxProps {}
const SideBar = (e?: Props) => { const SideBar = (e?: Props) => {
const { const {
w = ['100%', '0 0 250px', '0 0 280px', '0 0 310px', '0 0 340px'], w = ['100%', '0 0 250px', '0 0 270px', '0 0 290px', '0 0 310px'],
children, children,
...props ...props
} = e || {}; } = e || {};
......
...@@ -10,7 +10,7 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => { ...@@ -10,7 +10,7 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
const theme = useMemo(() => { const theme = useMemo(() => {
const map = { const map = {
blue: { blue: {
borderColor: 'myBlue.700', borderColor: 'myBlue.600',
bg: '#F2FBFF', bg: '#F2FBFF',
color: 'myBlue.700' color: 'myBlue.700'
}, },
......
...@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth'; ...@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { modelId } = req.query as { modelId: string }; const { appId } = req.query as { appId: string };
if (!modelId) { if (!appId) {
throw new Error('参数错误'); throw new Error('参数错误');
} }
...@@ -20,28 +20,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -20,28 +20,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 验证是否是该用户的 model // 验证是否是该用户的 model
await authApp({ await authApp({
appId: modelId, appId,
userId userId
}); });
// 删除对应的聊天 // 删除对应的聊天
await Chat.deleteMany({ await Chat.deleteMany({
modelId appId
}); });
// 删除收藏列表 // 删除收藏列表
await Collection.deleteMany({ await Collection.deleteMany({
modelId modelId: appId
}); });
// 删除分享链接 // 删除分享链接
await ShareChat.deleteMany({ await ShareChat.deleteMany({
modelId appId
}); });
// 删除模型 // 删除模型
await App.deleteOne({ await App.deleteOne({
_id: modelId, _id: appId,
userId userId
}); });
......
...@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth'; ...@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { modelId } = req.query as { modelId: string }; const { appId } = req.query as { appId: string };
if (!modelId) { if (!appId) {
throw new Error('参数错误'); throw new Error('参数错误');
} }
...@@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase(); await connectToDatabase();
const { app } = await authApp({ const { app } = await authApp({
appId: modelId, appId,
userId, userId,
authOwner: false authOwner: false
}); });
......
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, App } from '@/service/mongo'; import { connectToDatabase, App } from '@/service/mongo';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
import type { AppListResponse } from '@/api/response/app'; import { AppListItemType } from '@/types/app';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
// 凭证校验 // 凭证校验
...@@ -13,41 +12,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -13,41 +12,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase(); await connectToDatabase();
// 根据 userId 获取模型信息 // 根据 userId 获取模型信息
const [myApps, myCollections] = await Promise.all([ const myApps = await App.find(
App.find( {
{ userId
userId },
}, '_id avatar name intro'
'_id avatar name intro' ).sort({
).sort({ updateTime: -1
updateTime: -1 });
}),
Collection.find({ userId })
.populate({
path: 'modelId',
select: '_id avatar name intro',
match: { 'share.isShare': true }
})
.then((res) => res.filter((item) => item.modelId))
]);
jsonRes<AppListResponse>(res, { jsonRes<AppListItemType[]>(res, {
data: { data: myApps
myApps: myApps.map((item) => ({
_id: item._id,
name: item.name,
avatar: item.avatar,
intro: item.intro
})),
myCollectionApps: myCollections
.map((item: any) => ({
_id: item.modelId?._id,
name: item.modelId?.name,
avatar: item.modelId?.avatar,
intro: item.modelId?.intro
}))
.filter((item) => !myApps.find((model) => String(model._id) === String(item._id))) // 去重
}
}); });
} catch (err) { } catch (err) {
jsonRes(res, { jsonRes(res, {
......
...@@ -6,9 +6,9 @@ import { authUser } from '@/service/utils/auth'; ...@@ -6,9 +6,9 @@ import { authUser } from '@/service/utils/auth';
/* 模型收藏切换 */ /* 模型收藏切换 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { modelId } = req.query as { modelId: string }; const { appId } = req.query as { appId: string };
if (!modelId) { if (!appId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
// 凭证校验 // 凭证校验
...@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const collectionRecord = await Collection.findOne({ const collectionRecord = await Collection.findOne({
userId, userId,
modelId modelId: appId
}); });
if (collectionRecord) { if (collectionRecord) {
...@@ -26,12 +26,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ...@@ -26,12 +26,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
} else { } else {
await Collection.create({ await Collection.create({
userId, userId,
modelId modelId: appId
}); });
} }
await App.findByIdAndUpdate(modelId, { await App.findByIdAndUpdate(appId, {
'share.collection': await Collection.countDocuments({ modelId }) 'share.collection': await Collection.countDocuments({ modelId: appId })
}); });
jsonRes(res); jsonRes(res);
......
...@@ -6,14 +6,15 @@ import { sseResponseEventEnum } from '@/constants/chat'; ...@@ -6,14 +6,15 @@ import { sseResponseEventEnum } from '@/constants/chat';
import { sseResponse } from '@/service/utils/tools'; import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai'; import { type ChatCompletionRequestMessage } from 'openai';
import { AppModuleItemType } from '@/types/app'; import { AppModuleItemType } from '@/types/app';
import { dispatchModules } from '../openapi/v1/chat/completions2'; import { dispatchModules } from '../openapi/v1/chat/completions';
import { gptMessage2ChatType } from '@/utils/adapt';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string }; export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
export type Props = { export type Props = {
history: MessageItemType[]; history: MessageItemType[];
prompt: string; prompt: string;
modules: AppModuleItemType[]; modules: AppModuleItemType[];
variable: Record<string, any>; variables: Record<string, any>;
}; };
export type ChatResponseType = { export type ChatResponseType = {
newChatId: string; newChatId: string;
...@@ -29,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -29,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.end(); res.end();
}); });
let { modules = [], history = [], prompt, variable = {} } = req.body as Props; let { modules = [], history = [], prompt, variables = {} } = req.body as Props;
try { try {
if (!history || !modules || !prompt) { if (!history || !modules || !prompt) {
...@@ -48,9 +49,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -48,9 +49,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { responseData } = await dispatchModules({ const { responseData } = await dispatchModules({
res, res,
modules: modules, modules: modules,
variable, variables,
params: { params: {
history, history: gptMessage2ChatType(history),
userChatInput: prompt userChatInput: prompt
}, },
stream: true stream: true
......
...@@ -5,12 +5,10 @@ import { authUser } from '@/service/utils/auth'; ...@@ -5,12 +5,10 @@ import { authUser } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { chatId, contentId } = req.query as { const { historyId, contentId } = req.query as { historyId: string; contentId: string };
chatId: string; console.log(historyId, contentId);
contentId: string;
};
if (!chatId || !contentId) { if (!historyId || !contentId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
...@@ -19,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -19,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 凭证校验 // 凭证校验
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
const chatRecord = await Chat.findById(chatId); const chatRecord = await Chat.findById(historyId);
if (!chatRecord) { if (!chatRecord) {
throw new Error('找不到对话'); throw new Error('找不到对话');
...@@ -28,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -28,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 删除一条数据库记录 // 删除一条数据库记录
await Chat.updateOne( await Chat.updateOne(
{ {
_id: chatId, _id: historyId,
userId userId
}, },
{ $pull: { content: { _id: contentId } } } { $pull: { content: { _id: contentId } } }
......
...@@ -2,31 +2,32 @@ import type { NextApiRequest, NextApiResponse } from 'next'; ...@@ -2,31 +2,32 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
import type { HistoryItemType } from '@/types/chat'; import type { ChatHistoryItemType } from '@/types/chat';
/* 获取历史记录 */ /* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { appId } = req.body as { appId?: string };
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
await connectToDatabase(); await connectToDatabase();
const data = await Chat.find( const data = await Chat.find(
{ {
userId userId,
...(appId && { appId })
}, },
'_id title top customTitle modelId updateTime latestChat' '_id title top customTitle appId updateTime'
) )
.sort({ top: -1, updateTime: -1 }) .sort({ top: -1, updateTime: -1 })
.limit(20); .limit(20);
jsonRes<HistoryItemType[]>(res, { jsonRes<ChatHistoryItemType[]>(res, {
data: data.map((item) => ({ data: data.map((item) => ({
_id: item._id, _id: item._id,
updateTime: item.updateTime, updateTime: item.updateTime,
modelId: item.modelId, appId: item.appId,
title: item.customTitle || item.title, title: item.customTitle || item.title,
latestChat: item.latestChat,
top: item.top top: item.top
})) }))
}); });
......
...@@ -4,7 +4,7 @@ import { connectToDatabase, Chat } from '@/service/mongo'; ...@@ -4,7 +4,7 @@ import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth'; import { authUser } from '@/service/utils/auth';
export type Props = { export type Props = {
chatId: '' | string; historyId: string;
customTitle?: string; customTitle?: string;
top?: boolean; top?: boolean;
}; };
...@@ -12,7 +12,7 @@ export type Props = { ...@@ -12,7 +12,7 @@ export type Props = {
/* 更新聊天标题 */ /* 更新聊天标题 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { chatId, customTitle, top } = req.body as Props; const { historyId, customTitle, top } = req.body as Props;
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
...@@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await Chat.findOneAndUpdate( await Chat.findOneAndUpdate(
{ {
_id: chatId, _id: historyId,
userId userId
}, },
{ {
......
...@@ -6,23 +6,25 @@ import { authUser } from '@/service/utils/auth'; ...@@ -6,23 +6,25 @@ import { authUser } from '@/service/utils/auth';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { authApp } from '@/service/utils/auth'; import { authApp } from '@/service/utils/auth';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import type { AppSchema } from '@/types/mongoSchema'; import type { AppSchema, ChatSchema } from '@/types/mongoSchema';
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 {
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
let { modelId, chatId } = req.query as { let { appId, historyId } = req.query as {
modelId: '' | string; appId: '' | string;
chatId: '' | string; historyId: '' | string;
}; };
await connectToDatabase(); await connectToDatabase();
// 没有 modelId 时,直接获取用户的第一个id // 没有 appId 时,直接获取用户的第一个id
const app = await (async () => { const app = await (async () => {
if (!modelId) { if (!appId) {
const myModel = await App.findOne({ userId }); const myModel = await App.findOne({ userId });
if (!myModel) { if (!myModel) {
const { _id } = await App.create({ const { _id } = await App.create({
...@@ -36,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -36,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} else { } else {
// 校验使用权限 // 校验使用权限
const authRes = await authApp({ const authRes = await authApp({
appId: modelId, appId,
userId, userId,
authUser: false, authUser: false,
authOwner: false authOwner: false
...@@ -45,63 +47,71 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -45,63 +47,71 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
})(); })();
modelId = modelId || app._id; appId = appId || app._id;
// 历史记录 // 历史记录
let history: ChatItemType[] = []; const { chat, history = [] }: { chat?: ChatSchema; history?: ChatItemType[] } =
await (async () => {
if (chatId) { if (historyId) {
// auth chatId // auth chatId
const chat = await Chat.countDocuments({ const chat = await Chat.findOne({
_id: chatId, _id: historyId,
userId userId
}); });
if (chat === 0) { if (!chat) {
throw new Error('聊天框不存在'); throw new Error('聊天框不存在');
}
// 获取 chat.content 数据
history = await Chat.aggregate([
{
$match: {
_id: new mongoose.Types.ObjectId(chatId),
userId: new mongoose.Types.ObjectId(userId)
} }
}, // 获取 chat.content 数据
{ const history = await Chat.aggregate([
$project: { {
content: { $match: {
$slice: ['$content', -50] // 返回 content 数组的最后50个元素 _id: new mongoose.Types.ObjectId(historyId),
userId: new mongoose.Types.ObjectId(userId)
}
},
{
$project: {
content: {
$slice: ['$content', -50] // 返回 content 数组的最后50个元素
}
}
},
{ $unwind: '$content' },
{
$project: {
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
systemPrompt: '$content.systemPrompt',
quoteLen: { $size: { $ifNull: ['$content.quote', []] } }
}
} }
} ]);
}, return { history, chat };
{ $unwind: '$content' },
{
$project: {
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
systemPrompt: '$content.systemPrompt',
quoteLen: { $size: { $ifNull: ['$content.quote', []] } }
}
} }
]); return {};
} })();
const isOwner = String(app.userId) === userId; const isOwner = String(app.userId) === userId;
jsonRes<InitChatResponse>(res, { jsonRes<InitChatResponse>(res, {
data: { data: {
chatId: chatId || '', historyId,
modelId: modelId, appId,
model: { 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,
canUse: app.share.isShare || isOwner canUse: app.share.isShare || isOwner
}, },
chatModel: app.chat.chatModel, title: chat?.title || '新对话',
systemPrompt: isOwner ? app.chat.systemPrompt : '', variables: chat?.variables || {},
limitPrompt: isOwner ? app.chat.limitPrompt : '',
history history
} }
}); });
......
...@@ -7,15 +7,16 @@ import { authUser } from '@/service/utils/auth'; ...@@ -7,15 +7,16 @@ import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
type Props = { type Props = {
chatId?: string; historyId?: string;
modelId: string; appId: string;
variables?: Record<string, any>;
prompts: [ChatItemType, ChatItemType]; prompts: [ChatItemType, ChatItemType];
}; };
/* 聊天内容存存储 */ /* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { chatId, modelId, prompts } = req.body as Props; const { historyId, appId, prompts } = req.body as Props;
if (!prompts) { if (!prompts) {
throw new Error('缺少参数'); throw new Error('缺少参数');
...@@ -24,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -24,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true }); const { userId } = await authUser({ req, authToken: true });
const response = await saveChat({ const response = await saveChat({
chatId, historyId,
modelId, appId,
prompts, prompts,
userId userId
}); });
...@@ -42,14 +43,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -42,14 +43,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
export async function saveChat({ export async function saveChat({
newChatId, newHistoryId,
chatId, historyId,
modelId, appId,
prompts, prompts,
variables,
userId userId
}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> { }: Props & { newHistoryId?: Types.ObjectId; userId: string }): Promise<{ newHistoryId: string }> {
await connectToDatabase(); await connectToDatabase();
const { app } = await authApp({ appId: modelId, userId, authOwner: false }); const { app } = await authApp({ appId, userId, authOwner: false });
const content = prompts.map((item) => ({ const content = prompts.map((item) => ({
_id: item._id, _id: item._id,
...@@ -60,43 +62,45 @@ export async function saveChat({ ...@@ -60,43 +62,45 @@ export async function saveChat({
})); }));
if (String(app.userId) === userId) { if (String(app.userId) === userId) {
await App.findByIdAndUpdate(modelId, { await App.findByIdAndUpdate(appId, {
updateTime: new Date() updateTime: new Date()
}); });
} }
const [response] = await Promise.all([ const [response] = await Promise.all([
...(chatId ...(historyId
? [ ? [
Chat.findByIdAndUpdate(chatId, { Chat.findByIdAndUpdate(historyId, {
$push: { $push: {
content: { content: {
$each: content $each: content
} }
}, },
variables,
title: content[0].value.slice(0, 20), title: content[0].value.slice(0, 20),
latestChat: content[1].value, latestChat: content[1].value,
updateTime: new Date() updateTime: new Date()
}).then(() => ({ }).then(() => ({
newChatId: '' newHistoryId: ''
})) }))
] ]
: [ : [
Chat.create({ Chat.create({
_id: newChatId, _id: newHistoryId,
userId, userId,
modelId, appId,
variables,
content, content,
title: content[0].value.slice(0, 20), title: content[0].value.slice(0, 20),
latestChat: content[1].value latestChat: content[1].value
}).then((res) => ({ }).then((res) => ({
newChatId: String(res._id) newHistoryId: String(res._id)
})) }))
]), ]),
// update app // update app
...(String(app.userId) === userId ...(String(app.userId) === userId
? [ ? [
App.findByIdAndUpdate(modelId, { App.findByIdAndUpdate(appId, {
updateTime: new Date() updateTime: new Date()
}) })
] ]
...@@ -105,6 +109,6 @@ export async function saveChat({ ...@@ -105,6 +109,6 @@ export async function saveChat({
return { return {
// @ts-ignore // @ts-ignore
newChatId: response?.newChatId || '' newHistoryId: response?.newHistoryId || ''
}; };
} }
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, getApiKey } from '@/service/utils/auth';
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { appKbSearch } from '../kb/appKbSearch';
/* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
try {
const {
chatId,
prompts,
modelId,
isStream = true
} = req.body as {
chatId?: string;
prompts: ChatItemType[];
modelId: string;
isStream: boolean;
};
if (!prompts || !modelId) {
throw new Error('缺少参数');
}
if (!Array.isArray(prompts)) {
throw new Error('prompts is not array');
}
if (prompts.length > 30 || prompts.length === 0) {
throw new Error('Prompts arr length range 1-30');
}
await connectToDatabase();
let startTime = Date.now();
/* 凭证校验 */
const { userId } = await authUser({ req });
const { app } = await authApp({
userId,
appId: modelId
});
/* get api key */
const { systemAuthKey: apiKey } = await getApiKey({
model: app.chat.chatModel,
userId,
mustPay: true
});
const modelConstantsData = ChatModelMap[app.chat.chatModel];
const prompt = prompts[prompts.length - 1];
const {
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (app.chat.relatedKbs?.length > 0) {
const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model: app,
userId,
fixedQuote: [],
prompt: prompt,
similarity: app.chat.searchSimilarity,
limit: app.chat.searchLimit
});
return {
userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt]
};
}
return {
userSystemPrompt: app.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: app.chat.systemPrompt
}
]
: [],
userLimitPrompt: app.chat.limitPrompt
? [
{
obj: ChatRoleEnum.Human,
value: app.chat.limitPrompt
}
]
: []
};
})();
// search result is empty
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
const response = app.chat.searchEmptyText;
return res.end(response);
}
// 读取对话内容
const completePrompts = [
...quotePrompt,
...userSystemPrompt,
...prompts.slice(0, -1),
...userLimitPrompt,
prompt
];
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
2
);
// 发出请求
const { streamResponse, responseMessages, responseText, totalTokens } =
await modelServiceToolMap.chatCompletion({
model: app.chat.chatModel,
apiKey,
temperature: +temperature,
messages: completePrompts,
stream: isStream,
res
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
const { textLen = 0, tokens = totalTokens } = await (async () => {
if (isStream) {
try {
const { finishMessages, totalTokens } = await resStreamResponse({
model: app.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
res.end();
return {
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens
};
} catch (error) {
res.end();
console.log('error,结束', error);
}
} else {
jsonRes(res, {
data: responseText
});
return {
textLen: responseMessages.map((item) => item.value).join('').length
};
}
return {};
})();
pushChatBill({
isPay: true,
chatModel: app.chat.chatModel,
userId,
textLen,
tokens,
type: BillTypeEnum.openapiChat
});
} catch (err: any) {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
});
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth'; import { authUser, authApp, authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat'; import { sseErrRes, jsonRes } from '@/service/response';
import { jsonRes } from '@/service/response';
import { ChatModelMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat'; import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools'; import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user';
import { appKbSearch } from '../../../openapi/kb/appKbSearch';
import type { CreateChatCompletionRequest } from 'openai'; import type { CreateChatCompletionRequest } from 'openai';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt'; import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory'; import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat'; import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools'; import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai'; import { type ChatCompletionRequestMessage } from 'openai';
import { SpecificInputEnum, AppModuleItemTypeEnum } from '@/constants/app';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { sensitiveCheck } from '../../text/sensitiveCheck'; import { moduleFetch } from '@/service/api/request';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { FlowInputItemTypeEnum } from '@/constants/flow';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string }; export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = { type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history historyId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
appId?: string; appId?: string;
}; };
type FastGptShareChatProps = { type FastGptShareChatProps = {
password?: string;
shareId?: string; shareId?: string;
}; };
export type Props = CreateChatCompletionRequest & export type Props = CreateChatCompletionRequest &
FastGptWebChatProps & FastGptWebChatProps &
FastGptShareChatProps & { FastGptShareChatProps & {
messages: MessageItemType[]; messages: MessageItemType[];
stream?: boolean;
variables: Record<string, any>;
}; };
export type ChatResponseType = { export type ChatResponseType = {
newChatId: string; newChatId: string;
quoteLen?: number; quoteLen?: number;
}; };
/* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) { export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => { res.on('close', () => {
res.end(); res.end();
...@@ -47,8 +45,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -47,8 +45,14 @@ 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 {
let step = 0; historyId,
appId,
shareId,
stream = false,
messages = [],
variables = {}
} = req.body as Props;
try { try {
if (!messages) { if (!messages) {
...@@ -68,8 +72,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -68,8 +72,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 }));
...@@ -78,257 +81,96 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -78,257 +81,96 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('appId is empty'); throw new Error('appId is empty');
} }
// auth app permission // auth app, get history
const { app, showModelDetail } = await authApp({ const [{ app }, { history }] = await Promise.all([
userId, authApp({
appId, appId,
authOwner: false, userId
reserveDetail: true }),
}); getChatHistory({ historyId, userId })
]);
const showAppDetail = !shareId && showModelDetail;
/* get api key */
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
model: app.chat.chatModel,
userId,
mustPay: authType !== 'token'
});
// get history
const { history } = await getChatHistory({ chatId, userId });
const prompts = history.concat(gptMessage2ChatType(messages)); const prompts = history.concat(gptMessage2ChatType(messages));
// adapt fastgpt web
if (prompts[prompts.length - 1].obj === 'AI') { if (prompts[prompts.length - 1].obj === 'AI') {
prompts.pop(); prompts.pop();
} }
// user question // user question
const prompt = prompts[prompts.length - 1]; const prompt = prompts.pop();
const { if (!prompt) {
rawSearch = [], throw new Error('Question is empty');
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (app.chat.relatedKbs?.length > 0) {
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model: app,
userId,
fixedQuote: history[history.length - 1]?.quote,
prompt,
similarity: app.chat.searchSimilarity,
limit: app.chat.searchLimit
});
return {
rawSearch,
userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt]
};
}
return {
userSystemPrompt: app.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: app.chat.systemPrompt
}
]
: [],
userLimitPrompt: app.chat.limitPrompt
? [
{
obj: ChatRoleEnum.Human,
value: app.chat.limitPrompt
}
]
: []
};
})();
// search result is empty
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
const response = app.chat.searchEmptyText;
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: response,
model: app.chat.chatModel,
finish_reason: 'stop'
})
});
return res.end();
} else {
return res.json({
id: chatId || '',
object: 'chat.completion',
created: 1688608930,
model: app.chat.chatModel,
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
choices: [
{ message: { role: 'assistant', content: response }, finish_reason: 'stop', index: 0 }
]
});
}
} }
// api messages. [quote,context,systemPrompt,question] const newHistoryId = historyId === '' ? new Types.ObjectId() : undefined;
const completePrompts = [ if (stream && newHistoryId) {
...quotePrompt, res.setHeader('newHistoryId', String(newHistoryId));
...userSystemPrompt, }
...prompts.slice(0, -1),
...userLimitPrompt,
prompt
];
// chat temperature
const modelConstantsData = ChatModelMap[app.chat.chatModel];
// FastGpt temperature range: 1~10
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
2
);
await sensitiveCheck({ /* start process */
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}` const { responseData, answerText } = await dispatchModules({
res,
modules: app.modules,
variables,
params: {
history: prompts,
userChatInput: prompt.value
},
stream
}); });
// start app api. responseText and totalTokens: valid only if stream = false // save chat
const { streamResponse, responseMessages, responseText, totalTokens } = if (typeof historyId === 'string') {
await modelServiceToolMap.chatCompletion({
model: app.chat.chatModel,
apiKey: userOpenAiKey || apiKey,
temperature: +temperature,
maxToken: app.chat.maxToken,
messages: completePrompts,
stream,
res
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
if (res.closed) return res.end();
// create a chatId
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
// response answer
const {
textLen = 0,
answer = responseText,
tokens = totalTokens
} = await (async () => {
if (stream) {
// 创建响应流
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Transfer-Encoding', 'chunked');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
try {
// response newChatId and quota
sseResponse({
res,
event: sseResponseEventEnum.chatResponse,
data: JSON.stringify({
newChatId,
quoteLen: rawSearch.length
})
});
// response answer
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
model: app.chat.chatModel,
res,
chatResponse: streamResponse,
prompts: responseMessages
});
return {
answer: responseContent,
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens
};
} catch (error) {
return Promise.reject(error);
}
} else {
return {
textLen: responseMessages.map((item) => item.value).join('').length
};
}
})();
// save chat history
if (typeof chatId === 'string') {
await saveChat({ await saveChat({
newChatId, historyId,
chatId, newHistoryId,
modelId: appId, appId,
prompts: [ prompts: [
prompt, prompt,
{ {
_id: messages[messages.length - 1]._id, _id: messages[messages.length - 1]._id,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: answer, value: answerText,
...(showAppDetail responseData
? {
quote: rawSearch,
systemPrompt: `${userSystemPrompt[0]?.value}\n\n${userLimitPrompt[0]?.value}`
}
: {})
} }
], ],
userId userId
}); });
} }
// close response
if (stream) { if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: '[DONE]'
});
sseResponse({
res,
event: sseResponseEventEnum.appStreamResponse,
data: JSON.stringify(responseData)
});
res.end(); res.end();
} else { } else {
res.json({ res.json({
...(showAppDetail data: {
? { newHistoryId,
rawSearch ...responseData
} },
: {}), id: historyId || '',
newChatId, model: '',
id: chatId || '', usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
object: 'chat.completion',
created: 1688608930,
model: app.chat.chatModel,
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
choices: [ choices: [
{ message: { role: 'assistant', content: answer }, finish_reason: 'stop', index: 0 } {
message: [{ role: 'assistant', content: answerText }],
finish_reason: 'stop',
index: 0
}
] ]
}); });
} }
pushChatBill({
isPay: !userOpenAiKey,
chatModel: app.chat.chatModel,
userId,
textLen,
tokens,
type: authType === 'apikey' ? BillTypeEnum.openapiChat : BillTypeEnum.chat
});
shareId &&
updateShareChatBill({
shareId,
tokens
});
} catch (err: any) { } catch (err: any) {
res.status(500); if (stream) {
if (step === 1) { res.status(500);
sseResponse({ sseErrRes(res, err);
res,
event: sseResponseEventEnum.error,
data: JSON.stringify(err)
});
res.end(); res.end();
} else { } else {
jsonRes(res, { jsonRes(res, {
...@@ -338,3 +180,232 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ...@@ -338,3 +180,232 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} }
} }
}); });
export async function dispatchModules({
res,
modules,
params = {},
variables = {},
stream = false
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
params?: Record<string, any>;
variables?: Record<string, any>;
stream?: boolean;
}) {
const runningModules = loadModules(modules, variables);
let storeData: Record<string, any> = {};
let responseData: Record<string, any> = {};
let answerText = '';
function pushStore({
isResponse = false,
answer,
data = {}
}: {
isResponse?: boolean;
answer?: string;
data?: Record<string, any>;
}) {
if (isResponse) {
responseData = {
...responseData,
...data
};
}
if (answer) {
answerText += answer;
}
storeData = {
...storeData,
...data
};
}
function moduleInput(
module: RunningModuleItemType,
data: Record<string, any> = {}
): Promise<any> {
const checkInputFinish = () => {
return !module.inputs.find((item: any) => item.value === undefined);
};
const updateInputValue = (key: string, value: any) => {
const index = module.inputs.findIndex((item: any) => item.key === key);
if (index === -1) return;
module.inputs[index].value = value;
};
const set = new Set();
return Promise.all(
Object.entries(data).map(([key, val]: any) => {
updateInputValue(key, val);
if (!set.has(module.moduleId) && checkInputFinish()) {
set.add(module.moduleId);
return moduleRun(module);
}
})
);
}
function moduleOutput(
module: RunningModuleItemType,
result: Record<string, any> = {}
): Promise<any> {
return Promise.all(
module.outputs.map((outputItem) => {
if (result[outputItem.key] === undefined) return;
/* update output value */
outputItem.value = result[outputItem.key];
pushStore({
isResponse: outputItem.response,
answer: outputItem.answer ? outputItem.value : '',
data: {
[outputItem.key]: outputItem.value
}
});
/* update target */
return Promise.all(
outputItem.targets.map((target: any) => {
// find module
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
if (!targetModule) return;
return moduleInput(targetModule, { [target.key]: outputItem.value });
})
);
})
);
}
async function moduleRun(module: RunningModuleItemType): Promise<any> {
if (res.closed) return Promise.resolve();
console.log('run=========', module.type, module.url);
// direct answer
if (module.type === AppModuleItemTypeEnum.answer) {
const text =
module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value || '';
pushStore({
answer: text
});
return StreamAnswer({
res,
stream,
text: text
});
}
if (module.type === AppModuleItemTypeEnum.switch) {
return moduleOutput(module, switchResponse(module));
}
if (
(module.type === AppModuleItemTypeEnum.http ||
module.type === AppModuleItemTypeEnum.initInput) &&
module.url
) {
// get fetch params
const params: Record<string, any> = {};
module.inputs.forEach((item: any) => {
params[item.key] = item.value;
});
const data = {
stream,
...params
};
// response data
const fetchRes = await moduleFetch({
res,
url: module.url,
data
});
return moduleOutput(module, fetchRes);
}
}
// start process width initInput
const initModules = runningModules.filter(
(item) => item.type === AppModuleItemTypeEnum.initInput
);
await Promise.all(initModules.map((module) => moduleInput(module, params)));
return {
responseData,
answerText
};
}
function loadModules(
modules: AppModuleItemType[],
variables: Record<string, any>
): RunningModuleItemType[] {
return modules.map((module) => {
return {
moduleId: module.moduleId,
type: module.type,
url: module.url,
inputs: module.inputs
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
.map((item) => {
if (typeof item.value !== 'string') {
return {
key: item.key,
value: item.value
};
}
// variables replace
const replacedVal = item.value.replace(
/{{(.*?)}}/g,
(match, key) => variables[key.trim()] || match
);
return {
key: item.key,
value: replacedVal
};
}),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.key === SpecificInputEnum.answerText,
response: item.response,
value: undefined,
targets: item.targets
}))
};
});
}
function StreamAnswer({
res,
stream = false,
text = ''
}: {
res: NextApiResponse;
stream?: boolean;
text?: string;
}) {
if (stream && text) {
return sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: text.replace(/\\n/g, '\n')
})
});
}
return text;
}
function switchResponse(module: RunningModuleItemType) {
const val = module?.inputs?.[0]?.value;
if (val) {
return { true: 1 };
}
return { false: 1 };
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
import { sseErrRes, jsonRes } from '@/service/response';
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools';
import type { CreateChatCompletionRequest } from 'openai';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai';
import { SpecificInputEnum, AppModuleItemTypeEnum } from '@/constants/app';
import { model, Types } from 'mongoose';
import { moduleFetch } from '@/service/api/request';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum } from '@/constants/flow';
import { SystemInputEnum } from '@/constants/app';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
appId?: string;
};
type FastGptShareChatProps = {
shareId?: string;
};
export type Props = CreateChatCompletionRequest &
FastGptWebChatProps &
FastGptShareChatProps & {
messages: MessageItemType[];
stream?: boolean;
variables: Record<string, any>;
};
export type ChatResponseType = {
newChatId: string;
quoteLen?: number;
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
res.on('error', () => {
console.log('error: ', 'request error');
res.end();
});
let { chatId, appId, shareId, stream = false, messages = [], variables = {} } = req.body as Props;
try {
if (!messages) {
throw new Error('Prams Error');
}
if (!Array.isArray(messages)) {
throw new Error('messages is not array');
}
await connectToDatabase();
let startTime = Date.now();
/* user auth */
const {
userId,
appId: authAppid,
authType
} = await (shareId
? authShareChat({
shareId
})
: authUser({ req }));
appId = appId ? appId : authAppid;
if (!appId) {
throw new Error('appId is empty');
}
// auth app, get history
const [{ app }, { history }] = await Promise.all([
authApp({
appId,
userId
}),
getChatHistory({ chatId, userId })
]);
const prompts = history.concat(gptMessage2ChatType(messages));
if (prompts[prompts.length - 1].obj === 'AI') {
prompts.pop();
}
// user question
const prompt = prompts.pop();
if (!prompt) {
throw new Error('Question is empty');
}
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
if (stream && newChatId) {
res.setHeader('newChatId', String(newChatId));
}
/* start process */
const { responseData, answerText } = await dispatchModules({
res,
modules: app.modules,
variables,
params: {
history: prompts,
userChatInput: prompt.value
},
stream
});
// save chat
if (typeof chatId === 'string') {
await saveChat({
chatId,
newChatId,
modelId: appId,
prompts: [
prompt,
{
_id: messages[messages.length - 1]._id,
obj: ChatRoleEnum.AI,
value: answerText,
responseData
}
],
userId
});
}
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: '[DONE]'
});
sseResponse({
res,
event: sseResponseEventEnum.appStreamResponse,
data: JSON.stringify(responseData)
});
res.end();
} else {
res.json({
data: {
newChatId,
...responseData
},
id: chatId || '',
model: '',
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
choices: [
{
message: [{ role: 'assistant', content: answerText }],
finish_reason: 'stop',
index: 0
}
]
});
}
} catch (err: any) {
if (stream) {
res.status(500);
sseErrRes(res, err);
res.end();
} else {
jsonRes(res, {
code: 500,
error: err
});
}
}
});
export async function dispatchModules({
res,
modules,
params = {},
variables = {},
stream = false
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
params?: Record<string, any>;
variables?: Record<string, any>;
stream?: boolean;
}) {
const runningModules = loadModules(modules, variables);
let storeData: Record<string, any> = {};
let responseData: Record<string, any> = {};
let answerText = '';
function pushStore({
isResponse = false,
answer,
data = {}
}: {
isResponse?: boolean;
answer?: string;
data?: Record<string, any>;
}) {
if (isResponse) {
responseData = {
...responseData,
...data
};
}
if (answer) {
answerText += answer;
}
storeData = {
...storeData,
...data
};
}
function moduleInput(
module: RunningModuleItemType,
data: Record<string, any> = {}
): Promise<any> {
const checkInputFinish = () => {
return !module.inputs.find((item: any) => item.value === undefined);
};
const updateInputValue = (key: string, value: any) => {
const index = module.inputs.findIndex((item: any) => item.key === key);
if (index === -1) return;
module.inputs[index].value = value;
};
const set = new Set();
return Promise.all(
Object.entries(data).map(([key, val]: any) => {
updateInputValue(key, val);
if (!set.has(module.moduleId) && checkInputFinish()) {
set.add(module.moduleId);
return moduleRun(module);
}
})
);
}
function moduleOutput(
module: RunningModuleItemType,
result: Record<string, any> = {}
): Promise<any> {
return Promise.all(
module.outputs.map((outputItem) => {
if (result[outputItem.key] === undefined) return;
/* update output value */
outputItem.value = result[outputItem.key];
pushStore({
isResponse: outputItem.response,
answer: outputItem.answer ? outputItem.value : '',
data: {
[outputItem.key]: outputItem.value
}
});
/* update target */
return Promise.all(
outputItem.targets.map((target: any) => {
// find module
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
if (!targetModule) return;
return moduleInput(targetModule, { [target.key]: outputItem.value });
})
);
})
);
}
async function moduleRun(module: RunningModuleItemType): Promise<any> {
if (res.closed) return Promise.resolve();
console.log('run=========', module.type, module.url);
// direct answer
if (module.type === AppModuleItemTypeEnum.answer) {
const text =
module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value || '';
pushStore({
answer: text
});
return StreamAnswer({
res,
stream,
text: text
});
}
if (module.type === AppModuleItemTypeEnum.switch) {
return moduleOutput(module, switchResponse(module));
}
if (
(module.type === AppModuleItemTypeEnum.http ||
module.type === AppModuleItemTypeEnum.initInput) &&
module.url
) {
// get fetch params
const params: Record<string, any> = {};
module.inputs.forEach((item: any) => {
params[item.key] = item.value;
});
const data = {
stream,
...params
};
// response data
const fetchRes = await moduleFetch({
res,
url: module.url,
data
});
return moduleOutput(module, fetchRes);
}
}
// start process width initInput
const initModules = runningModules.filter(
(item) => item.type === AppModuleItemTypeEnum.initInput
);
await Promise.all(initModules.map((module) => moduleInput(module, params)));
return {
responseData,
answerText
};
}
function loadModules(
modules: AppModuleItemType[],
variables: Record<string, any>
): RunningModuleItemType[] {
return modules.map((module) => {
return {
moduleId: module.moduleId,
type: module.type,
url: module.url,
inputs: module.inputs
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
.map((item) => {
if (typeof item.value !== 'string') {
return {
key: item.key,
value: item.value
};
}
// variables replace
const replacedVal = item.value.replace(
/{{(.*?)}}/g,
(match, key) => variables[key.trim()] || match
);
return {
key: item.key,
value: replacedVal
};
}),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.key === SpecificInputEnum.answerText,
response: item.response,
value: undefined,
targets: item.targets
}))
};
});
}
function StreamAnswer({
res,
stream = false,
text = ''
}: {
res: NextApiResponse;
stream?: boolean;
text?: string;
}) {
if (stream && text) {
return sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: text.replace(/\\n/g, '\n')
})
});
}
return text;
}
function switchResponse(module: RunningModuleItemType) {
const val = module?.inputs?.[0]?.value;
if (val) {
return { true: 1 };
}
return { false: 1 };
}
...@@ -7,7 +7,7 @@ import { Types } from 'mongoose'; ...@@ -7,7 +7,7 @@ import { Types } from 'mongoose';
import type { ChatItemType } from '@/types/chat'; import type { ChatItemType } from '@/types/chat';
export type Props = { export type Props = {
chatId?: string; historyId?: string;
limit?: number; limit?: number;
}; };
export type Response = { history: ChatItemType[] }; export type Response = { history: ChatItemType[] };
...@@ -16,11 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -16,11 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try { try {
await connectToDatabase(); await connectToDatabase();
const { userId } = await authUser({ req }); const { userId } = await authUser({ req });
const { chatId, limit } = req.body as Props; const { historyId, limit } = req.body as Props;
jsonRes<Response>(res, { jsonRes<Response>(res, {
data: await getChatHistory({ data: await getChatHistory({
chatId, historyId,
userId, userId,
limit limit
}) })
...@@ -34,16 +34,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -34,16 +34,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
export async function getChatHistory({ export async function getChatHistory({
chatId, historyId,
userId, userId,
limit = 50 limit = 50
}: Props & { userId: string }): Promise<Response> { }: Props & { userId: string }): Promise<Response> {
if (!chatId) { if (!historyId) {
return { history: [] }; return { history: [] };
} }
const history = await Chat.aggregate([ const history = await Chat.aggregate([
{ $match: { _id: new Types.ObjectId(chatId), userId: new Types.ObjectId(userId) } }, { $match: { _id: new Types.ObjectId(historyId), userId: new Types.ObjectId(userId) } },
{ {
$project: { $project: {
content: { content: {
......
...@@ -58,7 +58,6 @@ const ChatTest = ( ...@@ -58,7 +58,6 @@ const ChatTest = (
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode) ?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0; ?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = messages.slice(-historyMaxLen - 2, -2); const history = messages.slice(-historyMaxLen - 2, -2);
console.log(history, 'history====');
// 流请求,获取数据 // 流请求,获取数据
const { responseText } = await streamFetch({ const { responseText } = await streamFetch({
...@@ -87,8 +86,6 @@ const ChatTest = ( ...@@ -87,8 +86,6 @@ const ChatTest = (
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
resetChatTest() { resetChatTest() {
console.log(ChatBoxRef.current, '===');
ChatBoxRef.current?.resetHistory([]); ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables(); ChatBoxRef.current?.resetVariables();
} }
...@@ -147,6 +144,7 @@ const ChatTest = ( ...@@ -147,6 +144,7 @@ const ChatTest = (
variableModules={variableModules} variableModules={variableModules}
welcomeText={welcomeText} welcomeText={welcomeText}
onStartChat={startChat} onStartChat={startChat}
onDelMessage={() => {}}
/> />
</Box> </Box>
</Flex> </Flex>
......
...@@ -12,7 +12,7 @@ const ShareModelList = ({ ...@@ -12,7 +12,7 @@ const ShareModelList = ({
onclickCollection onclickCollection
}: { }: {
models: ShareAppItem[]; models: ShareAppItem[];
onclickCollection: (modelId: string) => void; onclickCollection: (appId: string) => void;
}) => { }) => {
const router = useRouter(); const router = useRouter();
......
...@@ -12,8 +12,6 @@ const modelList = () => { ...@@ -12,8 +12,6 @@ const modelList = () => {
const { Loading } = useLoading(); const { Loading } = useLoading();
const lastSearch = useRef(''); const lastSearch = useRef('');
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const { refreshModel } = useUserStore();
/* 加载模型 */ /* 加载模型 */
const { const {
data: models, data: models,
...@@ -30,16 +28,15 @@ const modelList = () => { ...@@ -30,16 +28,15 @@ const modelList = () => {
}); });
const onclickCollection = useCallback( const onclickCollection = useCallback(
async (modelId: string) => { async (appId: string) => {
try { try {
await triggerModelCollection(modelId); await triggerModelCollection(appId);
getData(pageNum); getData(pageNum);
refreshModel.removeModelDetail(modelId);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
}, },
[getData, pageNum, refreshModel] [getData, pageNum]
); );
return ( return (
......
import React from 'react'; import React, { useMemo } from 'react';
import { AddIcon } from '@chakra-ui/icons';
import { import {
Box, Box,
Button, Button,
...@@ -10,13 +9,16 @@ import { ...@@ -10,13 +9,16 @@ import {
MenuList, MenuList,
MenuItem MenuItem
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
type HistoryItemType = {
id: string;
title: string;
top?: boolean;
};
const ChatHistorySlider = ({ const ChatHistorySlider = ({
appName, appName,
appAvatar, appAvatar,
...@@ -24,23 +26,26 @@ const ChatHistorySlider = ({ ...@@ -24,23 +26,26 @@ const ChatHistorySlider = ({
activeHistoryId, activeHistoryId,
onChangeChat, onChangeChat,
onDelHistory, onDelHistory,
onSetHistoryTop,
onCloseSlider onCloseSlider
}: { }: {
appName: string; appName: string;
appAvatar: string; appAvatar: string;
history: { history: HistoryItemType[];
id: string;
title: string;
}[];
activeHistoryId: string; activeHistoryId: string;
onChangeChat: (historyId?: string) => void; onChangeChat: (historyId?: string) => void;
onDelHistory: (historyId: string) => void; onDelHistory: (historyId: string) => void;
onSetHistoryTop?: (e: { historyId: string; top: boolean }) => void;
onCloseSlider: () => void; onCloseSlider: () => void;
}) => { }) => {
const router = useRouter();
const theme = useTheme(); const theme = useTheme();
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
const concatHistory = useMemo<HistoryItemType[]>(
() => (!activeHistoryId ? [{ id: activeHistoryId, title: '新对话' }].concat(history) : history),
[activeHistoryId, history]
);
return ( return (
<Flex <Flex
position={'relative'} position={'relative'}
...@@ -48,11 +53,11 @@ const ChatHistorySlider = ({ ...@@ -48,11 +53,11 @@ const ChatHistorySlider = ({
w={'100%'} w={'100%'}
h={'100%'} h={'100%'}
bg={'white'} bg={'white'}
px={[2, 5]}
borderRight={['', theme.borders.base]} borderRight={['', theme.borders.base]}
whiteSpace={'nowrap'}
> >
{isPc && ( {isPc && (
<Flex pt={5} pb={2} alignItems={'center'} whiteSpace={'nowrap'}> <Flex pt={5} pb={2} px={[2, 5]} alignItems={'center'}>
<Avatar src={appAvatar} /> <Avatar src={appAvatar} />
<Box ml={2} fontWeight={'bold'} className={'textEllipsis'}> <Box ml={2} fontWeight={'bold'} className={'textEllipsis'}>
{appName} {appName}
...@@ -60,7 +65,7 @@ const ChatHistorySlider = ({ ...@@ -60,7 +65,7 @@ const ChatHistorySlider = ({
</Flex> </Flex>
)} )}
{/* 新对话 */} {/* 新对话 */}
<Box w={'100%'} h={'36px'} my={5}> <Box w={'100%'} px={[2, 5]} h={'36px'} my={5}>
<Button <Button
variant={'base'} variant={'base'}
w={'100%'} w={'100%'}
...@@ -76,8 +81,8 @@ const ChatHistorySlider = ({ ...@@ -76,8 +81,8 @@ const ChatHistorySlider = ({
</Box> </Box>
{/* chat history */} {/* chat history */}
<Box flex={'1 0 0'} h={0} overflow={'overlay'}> <Box flex={'1 0 0'} h={0} px={[2, 5]} overflow={'overlay'}>
{history.map((item) => ( {concatHistory.map((item) => (
<Flex <Flex
position={'relative'} position={'relative'}
key={item.id} key={item.id}
...@@ -94,6 +99,7 @@ const ChatHistorySlider = ({ ...@@ -94,6 +99,7 @@ const ChatHistorySlider = ({
display: 'block' display: 'block'
} }
}} }}
bg={item.top ? '#E6F6F6 !important' : ''}
{...(item.id === activeHistoryId {...(item.id === activeHistoryId
? { ? {
backgroundColor: 'myBlue.100 !important', backgroundColor: 'myBlue.100 !important',
...@@ -109,49 +115,50 @@ const ChatHistorySlider = ({ ...@@ -109,49 +115,50 @@ const ChatHistorySlider = ({
<Box flex={'1 0 0'} ml={3} className="textEllipsis"> <Box flex={'1 0 0'} ml={3} className="textEllipsis">
{item.title} {item.title}
</Box> </Box>
<Box className="more" display={['block', 'none']}> {!!item.id && (
<Menu autoSelect={false} isLazy offset={[0, 5]}> <Box className="more" display={['block', 'none']}>
<MenuButton <Menu autoSelect={false} isLazy offset={[0, 5]}>
_hover={{ bg: 'white' }} <MenuButton
cursor={'pointer'} _hover={{ bg: 'white' }}
borderRadius={'md'} cursor={'pointer'}
onClick={(e) => { borderRadius={'md'}
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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelHistory(item.id);
if (item.id === activeHistoryId) {
onChangeChat();
}
}} }}
> >
<MyIcon mr={2} name={'delete'} w={'16px'}></MyIcon> <MyIcon name={'more'} w={'14px'} p={1} />
删除 </MenuButton>
</MenuItem> <MenuList color={'myGray.700'} minW={`90px !important`}>
</MenuList> {onSetHistoryTop && (
</Menu> <MenuItem
</Box> onClick={(e) => {
e.stopPropagation();
onSetHistoryTop({ historyId: item.id, top: !item.top });
}}
>
<MyIcon mr={2} name={'setTop'} w={'16px'}></MyIcon>
{item.top ? '取消置顶' : '置顶'}
</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> </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> </Box>
</Flex> </Flex>
); );
......
import React, { useCallback, useRef, useState, useMemo } 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 { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useLoading } from '@/hooks/useLoading';
import { useUserStore } from '@/store/user';
import MyIcon from '@/components/Icon';
import type { HistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import ModelList from './ModelList';
import { useGlobalStore } from '@/store/global';
import styles from '../index.module.scss';
import { useEditInfo } from '@/hooks/useEditInfo';
import { putChatHistory } from '@/api/chat';
import { useToast } from '@/hooks/useToast';
import { formatTimeToChatTime, getErrText } from '@/utils/tools';
const PcSliderBar = ({
onclickDelHistory,
onclickExportChat
}: {
onclickDelHistory: (historyId: string) => Promise<void>;
onclickExportChat: (type: ExportChatType) => void;
}) => {
const router = useRouter();
const { toast } = useToast();
const { modelId = '', chatId = '' } = router.query as {
modelId: string;
chatId: string;
};
const ContextMenuRef = useRef(null);
const onclickContext = useRef(false);
const theme = useTheme();
const { isPc } = useGlobalStore();
const { Loading, setIsLoading } = useLoading();
const [contextMenuData, setContextMenuData] = useState<{
left: number;
top: number;
history: HistoryItemType;
}>();
const { history, loadHistory } = useChatStore();
const { myApps, myCollectionApps, loadMyModels } = useUserStore();
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
title: '自定义历史记录标题',
placeholder: '如果设置为空,会自动跟随聊天记录。'
});
// 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: HistoryItemType) => {
e.preventDefault(); // 阻止默认右键菜单
if (!isPc) return;
onclickContext.current = true;
setContextMenuData({
left: e.clientX,
top: e.clientY,
history
});
},
[isPc]
);
useQuery(['loadModels'], loadMyModels);
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
loadHistory({ pageNum: 1 })
);
return (
<Flex
position={'relative'}
flexDirection={'column'}
w={'100%'}
h={'100%'}
bg={'white'}
borderRight={['', theme.borders.base]}
>
{/* 新对话 */}
{isPc && (
<Box
className={styles.newChat}
zIndex={1001}
w={'90%'}
h={'40px'}
my={5}
mx={'auto'}
position={'relative'}
>
<Button
variant={'base'}
w={'100%'}
h={'100%'}
leftIcon={<AddIcon />}
onClick={() => router.replace(`/chat?appId=${modelId}`)}
>
新对话
</Button>
{models.length > 1 && (
<Box
className={styles.modelListContainer}
position={'absolute'}
w={'115%'}
left={0}
top={'40px'}
transition={'0.15s ease-out'}
bg={'white'}
>
<Box
className={styles.modelList}
mt={'6px'}
h={'calc(100% - 6px)'}
overflow={'overlay'}
>
<ModelList models={models} modelId={modelId} />
</Box>
</Box>
)}
</Box>
)}
{/* chat history */}
<Box flex={'1 0 0'} h={0} pl={2} overflowY={'scroll'} userSelect={'none'}>
{history.map((item) => (
<Flex
key={item._id}
position={'relative'}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={[2, 0]}
cursor={'pointer'}
transition={'background-color .2s ease-in'}
_hover={{
backgroundImage: ['', theme.lgColor.hoverBlueGradient]
}}
{...(item._id === chatId
? {
backgroundImage: `${theme.lgColor.activeBlueGradient} !important`
}
: {
bg: item.top ? 'myGray.200' : ''
})}
onClick={() => {
if (item._id === chatId) return;
if (isPc) {
router.replace(`/chat?appId=${item.modelId}&chatId=${item._id}`);
} else {
router.push(`/chat?appId=${item.modelId}&chatId=${item._id}`);
}
}}
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={async (e) => {
e.stopPropagation();
setIsLoading(true);
try {
await onclickDelHistory(item._id);
} catch (error) {
console.log(error);
}
setIsLoading(false);
}}
/>
)}
</Flex>
))}
{!isLoadingHistory && 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>
{/* context menu */}
{contextMenuData && (
<Box zIndex={10} position={'fixed'} top={contextMenuData.top} left={contextMenuData.left}>
<Box ref={ContextMenuRef}></Box>
<Menu isOpen>
<MenuList>
<MenuItem
onClick={async () => {
try {
await putChatHistory({
chatId: contextMenuData.history._id,
top: !contextMenuData.history.top
});
loadHistory({ pageNum: 1, init: true });
} catch (error) {}
}}
>
{contextMenuData.history.top ? '取消置顶' : '置顶'}
</MenuItem>
<MenuItem
onClick={async () => {
setIsLoading(true);
try {
await onclickDelHistory(contextMenuData.history._id);
if (contextMenuData.history._id === chatId) {
router.replace(`/chat?appId=${modelId}`);
}
} catch (error) {
console.log(error);
}
setIsLoading(false);
}}
>
删除记录
</MenuItem>
<MenuItem
onClick={() =>
onOpenModal({
defaultVal: contextMenuData.history.title,
onSuccess: async (val: string) => {
await putChatHistory({
chatId: contextMenuData.history._id,
customTitle: val,
top: contextMenuData.history.top
});
toast({
title: '自定义标题成功',
status: 'success'
});
loadHistory({ pageNum: 1, init: true });
},
onError(err) {
toast({
title: getErrText(err),
status: 'error'
});
}
})
}
>
自定义标题
</MenuItem>
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('md')}>导出Markdown格式</MenuItem>
</MenuList>
</Menu>
</Box>
)}
<EditTitleModal />
<Loading loading={isLoadingHistory} fixed={false} />
</Flex>
);
};
export default PcSliderBar;
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { AppListItemType } from '@/types/app';
import Avatar from '@/components/Avatar';
const ModelList = ({ models, modelId }: { models: AppListItemType[]; modelId: string }) => {
const router = useRouter();
return (
<>
{models.map((item) => (
<Box key={item._id}>
<Flex
key={item._id}
position={'relative'}
alignItems={['flex-start', 'center']}
p={3}
cursor={'pointer'}
transition={'background-color .2s ease-in'}
borderLeft={['', '5px solid transparent']}
zIndex={0}
_hover={{
backgroundColor: ['', '#dee0e3']
}}
{...(modelId === item._id
? {
backgroundColor: '#eff0f1',
borderLeftColor: 'myBlue.600'
}
: {})}
onClick={() => {
router.replace(`/chat?appId=${item._id}`);
}}
>
<Avatar src={item.avatar} w={'34px'} h={'34px'} />
<Box flex={'1 0 0'} w={0} ml={3}>
<Box className="textEllipsis" color={'myGray.1000'}>
{item.name}
</Box>
</Box>
</Flex>
</Box>
))}
</>
);
};
export default ModelList;
import React, { useMemo, useState } from 'react';
import { AddIcon, ChatIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Flex,
Divider,
useDisclosure,
useColorMode,
useColorModeValue
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import WxConcat from '@/components/WxConcat';
import { delChatHistoryById } from '@/api/chat';
import { useChatStore } from '@/store/chat';
import Avatar from '@/components/Avatar';
import Tabs from '@/components/Tabs';
enum TabEnum {
app = 'app',
history = 'history'
}
const PhoneSliderBar = ({
chatId,
modelId,
onClose
}: {
chatId: string;
modelId: string;
onClose: () => void;
}) => {
const router = useRouter();
const [currentTab, setCurrentTab] = useState(TabEnum.app);
const { myApps, myCollectionApps, loadMyModels } = useUserStore();
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
useQuery(['loadModels'], loadMyModels);
const { history, loadHistory } = useChatStore();
useQuery(['loadingHistory'], () => loadHistory({ pageNum: 1 }));
const RenderButton = ({
onClick,
children
}: {
onClick: () => void;
children: JSX.Element | string;
}) => (
<Box px={3} mb={2}>
<Flex
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={onClick}
>
{children}
</Flex>
</Box>
);
return (
<Flex
flexDirection={'column'}
w={'100%'}
h={'100%'}
py={3}
backgroundColor={useColorModeValue('blackAlpha.800', 'blackAlpha.500')}
color={'white'}
>
<Flex mb={2} alignItems={'center'} justifyContent={'space-between'} px={2}>
<Tabs
w={'140px'}
list={[
{ label: '应用', id: TabEnum.app },
{ label: '历史记录', id: TabEnum.history }
]}
size={'sm'}
activeId={currentTab}
onChange={(e: any) => setCurrentTab(e)}
/>
{/* 新对话 */}
{currentTab === TabEnum.app && (
<Button
size={'sm'}
variant={'base'}
color={'white'}
leftIcon={<AddIcon />}
onClick={() => {
router.replace(`/chat?appId=${modelId}`);
onClose();
}}
>
新对话
</Button>
)}
</Flex>
{/* 我的模型 & 历史记录 折叠框*/}
<Box flex={'1 0 0'} px={3} h={0} overflowY={'auto'}>
{currentTab === TabEnum.app && (
<>
{models.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item._id === modelId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={async () => {
if (item._id === modelId) return;
router.replace(`/chat?appId=${item._id}`);
onClose();
}}
>
<Avatar src={item.avatar} mr={2} w={'18px'} h={'18px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>
</Flex>
))}
</>
)}
{currentTab === TabEnum.history && (
<>
{history.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item._id === chatId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={() => {
if (item._id === chatId) return;
router.replace(`/chat?appId=${item.modelId}&chatId=${item._id}`);
onClose();
}}
>
<ChatIcon mr={2} />
<Box flex={'1 0 0'} w={0} className="textEllipsis">
{item.title}
</Box>
<Box>
<MyIcon
name={'delete'}
w={'14px'}
onClick={async (e) => {
e.stopPropagation();
console.log(111);
await delChatHistoryById(item._id);
loadHistory({ pageNum: 1, init: true });
if (item._id === chatId) {
router.replace(`/chat?appId=${modelId}`);
}
}}
/>
</Box>
</Flex>
))}
</>
)}
</Box>
<Divider my={3} colorScheme={useColorModeValue('gray', 'white')} />
<RenderButton onClick={() => router.push('/model')}>
<>
<MyIcon name="out" fill={'white'} w={'18px'} h={'18px'} mr={4} />
退出聊天
</>
</RenderButton>
<RenderButton onClick={onOpenWx}>
<>
<MyIcon name="wx" fill={'white'} w={'18px'} h={'18px'} mr={4} />
交流群
</>
</RenderButton>
{/* wx 联系 */}
{isOpenWx && <WxConcat onClose={onCloseWx} />}
</Flex>
);
};
export default PhoneSliderBar;
import React from 'react';
import { Flex, Box, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import { useQuery } from '@tanstack/react-query';
import MyIcon from '@/components/Icon';
import Avatar from '@/components/Avatar';
const SliderApps = ({ appId }: { appId: string }) => {
const router = useRouter();
const { myApps, loadMyModels } = useUserStore();
useQuery(['loadModels'], loadMyModels);
return (
<>
<Flex
alignItems={'center'}
cursor={'pointer'}
py={2}
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.200' }}
onClick={() => router.replace('/app/list')}
>
<IconButton
mr={3}
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
bg={'white'}
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
h={'28px'}
size={'sm'}
borderRadius={'50%'}
aria-label={''}
/>
退出聊天
</Flex>
<Box mt={5}>
{myApps.map((item) => (
<Flex
key={item._id}
py={2}
px={3}
mb={3}
cursor={'pointer'}
borderRadius={'lg'}
alignItems={'center'}
{...(item._id === appId
? {
bg: 'white',
boxShadow: 'md'
}
: {
_hover: {
bg: 'myGray.200'
},
onClick: () => {
router.replace({
query: {
appId: item._id
}
});
}
})}
>
<Avatar src={item.avatar} w={'24px'} />
<Box ml={2} className={'textEllipsis'}>
{item.name}
</Box>
</Flex>
))}
</Box>
</>
);
};
export default SliderApps;
import React, { useCallback, useState, useRef, useMemo, useEffect, MouseEvent } from 'react'; import React, { useCallback, useState, useRef } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getInitChatSiteInfo, delChatRecordByIndex, delChatHistoryById } from '@/api/chat';
import type { ChatItemType, ChatSiteItemType, ExportChatType } from '@/types/chat';
import { import {
Textarea, getInitChatSiteInfo,
delChatRecordByIndex,
delChatHistoryById,
putChatHistory
} from '@/api/chat';
import {
Box, Box,
Flex, Flex,
useColorModeValue, useColorModeValue,
Menu,
MenuButton,
MenuList,
MenuItem,
Button,
Modal, Modal,
ModalOverlay, ModalOverlay,
ModalContent, ModalContent,
...@@ -22,895 +20,329 @@ import { ...@@ -22,895 +20,329 @@ import {
Drawer, Drawer,
DrawerOverlay, DrawerOverlay,
DrawerContent, DrawerContent,
Card,
useOutsideClick,
useTheme useTheme
} 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 dynamic from 'next/dynamic';
import { useCopyData, voiceBroadcast, hasVoiceApi, delay } from '@/utils/tools';
import { streamFetch } from '@/api/fetch'; import { streamFetch } from '@/api/fetch';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { Types } from 'mongoose';
import { ChatModelMap } from '@/constants/model';
import { useChatStore } from '@/store/chat'; import { useChatStore } from '@/store/chat';
import { useLoading } from '@/hooks/useLoading'; 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 SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import QuoteModal from './components/QuoteModal';
import { HUMAN_ICON } from '@/constants/chat';
import MyTooltip from '@/components/MyTooltip';
const Markdown = dynamic(async () => await import('@/components/Markdown'));
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
ssr: false
});
const History = dynamic(() => import('./components/History'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
import styles from './index.module.scss';
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
const textareaMinH = '22px'; import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import PageContainer from '@/components/PageContainer';
import SideBar from '@/components/SideBar';
import ChatHistorySlider from './components/ChatHistorySlider';
import SliderApps from './components/SliderApps';
import Tag from '@/components/Tag';
import { ChatHistoryItemType } from '@/types/chat';
const Chat = () => { const Chat = () => {
const router = useRouter(); const router = useRouter();
const { appId = '', chatId = '' } = router.query as { appId: string; chatId: string }; const { appId = '', historyId = '' } = router.query as { appId: string; historyId: string };
const theme = useTheme(); const theme = useTheme();
const ChatBox = useRef<HTMLDivElement>(null); const ChatBoxRef = useRef<ComponentRef>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null); const forbidRefresh = useRef(false);
const ContextMenuRef = useRef(null);
const PhoneContextShow = useRef(false);
// 中断请求
const controller = useRef(new AbortController());
const isLeavePage = useRef(false);
const [showHistoryQuote, setShowHistoryQuote] = useState<string>(); const [showHistoryQuote, setShowHistoryQuote] = useState<string>();
const [showSystemPrompt, setShowSystemPrompt] = useState(''); const [showSystemPrompt, setShowSystemPrompt] = useState('');
const [messageContextMenuData, setMessageContextMenuData] = useState<{
left: number;
top: number;
message: ChatSiteItemType;
}>();
const { const {
lastChatModelId, lastChatAppId,
setLastChatModelId, setLastChatAppId,
lastChatId, lastChatId,
setLastChatId, setLastChatId,
history,
loadHistory, loadHistory,
updateHistory,
chatData, chatData,
setChatData, setChatData
forbidLoadChatData,
setForbidLoadChatData
} = useChatStore(); } = useChatStore();
const isChatting = useMemo(
() => chatData.history[chatData.history.length - 1]?.status === 'loading',
[chatData.history]
);
const { toast } = useToast();
const { copyData } = useCopyData();
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
const { Loading, setIsLoading } = useLoading(); const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
// close contextMenu const startChat = useCallback(
useOutsideClick({ async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
ref: ContextMenuRef, const prompts = messages.slice(-2);
handler: () => { const { responseText, newHistoryId } = await streamFetch({
// 移动端长按后会将其设置为true,松手时候也会触发一次,松手的时候需要忽略一次。
if (PhoneContextShow.current) {
PhoneContextShow.current = false;
} else {
messageContextMenuData &&
setTimeout(() => {
setMessageContextMenuData(undefined);
window.getSelection?.()?.empty?.();
window.getSelection?.()?.removeAllRanges?.();
document?.getSelection()?.empty();
});
}
}
});
// 滚动到底部
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
if (!ChatBox.current) return;
ChatBox.current.scrollTo({
top: ChatBox.current.scrollHeight,
behavior
});
}, []);
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
// eslint-disable-next-line react-hooks/exhaustive-deps
const generatingMessage = useCallback(
throttle(() => {
if (!ChatBox.current) return;
const isBottom =
ChatBox.current.scrollTop + ChatBox.current.clientHeight + 150 >=
ChatBox.current.scrollHeight;
isBottom && scrollToBottom('auto');
}, 100),
[]
);
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
if (!TextareaDom.current) return;
TextareaDom.current.value = val;
setTimeout(() => {
/* 回到最小高度 */
if (TextareaDom.current) {
TextareaDom.current.style.height =
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
}
}, 100);
}, []);
// gpt 对话
const gptChatPrompt = useCallback(
async (prompts: ChatSiteItemType[]) => {
// create abort obj
const abortSignal = new AbortController();
controller.current = abortSignal;
isLeavePage.current = false;
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
// 流请求,获取数据
const { newChatId, errMsg } = await streamFetch({
data: { data: {
messages, messages: prompts,
chatId, variables,
appId, appId,
model: '' historyId
}, },
onMessage: (text: string) => { onMessage: generatingMessage,
setChatData((state) => ({ abortSignal: controller
...state,
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
...item,
value: item.value + text
};
})
}));
generatingMessage();
},
abortSignal
}); });
// 重置了页面,说明退出了当前聊天, 不缓存任何内容 const newTitle = prompts[0].content?.slice(0, 20) || '新对话';
if (isLeavePage.current) {
return;
}
// save chat // update history
if (newChatId) { if (newHistoryId) {
setForbidLoadChatData(true); forbidRefresh.current = true;
router.replace(`/chat?appId=${appId}&chatId=${newChatId}`); router.replace({
query: {
historyId: newHistoryId,
appId
}
});
const newHistory: ChatHistoryItemType = {
_id: newHistoryId,
updateTime: new Date(),
title: newTitle,
appId,
top: false
};
updateHistory(newHistory);
} else {
const currentHistory = history.find((item) => item._id === historyId);
currentHistory &&
updateHistory({
...currentHistory,
updateTime: new Date(),
title: newTitle
});
} }
// update chat window
abortSignal.signal.aborted && (await delay(500));
// 设置聊天内容为完成状态
setChatData((state) => ({ setChatData((state) => ({
...state, ...state,
chatId: newChatId || state.chatId, // 如果有 Id,说明是新创建的对话 title: newTitle,
history: state.history.map((item, index) => { history: ChatBoxRef.current?.getChatHistory() || state.history
if (index !== state.history.length - 1) return item;
return {
...item,
status: 'finish',
quoteLen: 0,
systemPrompt: `${chatData.systemPrompt}${`${
chatData.limitPrompt ? `\n\n${chatData.limitPrompt}` : ''
}`}`
};
})
})); }));
// refresh data return { responseText };
setTimeout(() => {
generatingMessage();
loadHistory({ pageNum: 1, init: true });
}, 100);
if (errMsg) {
toast({
status: 'warning',
title: errMsg
});
}
}, },
[ [appId, history, historyId, router, setChatData, updateHistory]
chatId,
appId,
setChatData,
generatingMessage,
setForbidLoadChatData,
router,
chatData.systemPrompt,
chatData.limitPrompt,
loadHistory,
toast
]
); );
/**
* 发送一个内容
*/
const sendPrompt = useCallback(async () => {
// get value
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[] = [
...chatData.history,
{
_id: String(new Types.ObjectId()),
obj: 'Human',
value: val,
status: 'finish'
},
{
_id: String(new Types.ObjectId()),
obj: 'AI',
value: '',
status: 'loading'
}
];
// 插入内容
setChatData((state) => ({
...state,
history: newChatList
}));
// 清空输入内容
resetInputVal('');
setTimeout(() => {
scrollToBottom();
}, 100);
try {
await gptChatPrompt(newChatList.slice(newChatList.length - 2));
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
status: 'warning',
duration: 5000,
isClosable: true
});
resetInputVal(value);
setChatData((state) => ({
...state,
history: newChatList.slice(0, newChatList.length - 2)
}));
}
}, [
isChatting,
chatData.history,
setChatData,
resetInputVal,
toast,
scrollToBottom,
gptChatPrompt
]);
// 删除一句话 // 删除一句话
const delChatRecord = useCallback( const delOneHistoryItem = useCallback(
async (index: number, historyId?: string) => { async ({ contentId, index }: { contentId?: string; index: number }) => {
if (!messageContextMenuData || !historyId) return; if (!historyId || !contentId) return;
setIsLoading(true);
try { try {
// 删除数据库最后一句
await delChatRecordByIndex(chatId, historyId);
setChatData((state) => ({ setChatData((state) => ({
...state, ...state,
history: state.history.filter((_, i) => i !== index) history: state.history.filter((_, i) => i !== index)
})); }));
await delChatRecordByIndex({ historyId, contentId });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
setIsLoading(false);
},
[chatId, messageContextMenuData, setChatData, setIsLoading]
);
// 复制内容
const onclickCopy = useCallback(
(value: string) => {
const val = value.replace(/\n+/g, '\n');
copyData(val);
}, },
[copyData] [historyId, setChatData]
); );
// delete a history
// export chat data const delHistoryById = useCallback(
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: chatData.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]();
},
[chatData.history]
);
// delete history and reload history
const onclickDelHistory = useCallback(
async (historyId: string) => { async (historyId: string) => {
await delChatHistoryById(historyId); await delChatHistoryById(historyId);
loadHistory({ pageNum: 1, init: true }); loadHistory({ appId });
},
[loadHistory]
);
// onclick chat message context
const onclickContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>, message: ChatSiteItemType) => {
e.preventDefault(); // 阻止默认右键菜单
// select all text
const range = document.createRange();
range.selectNodeContents(e.currentTarget as HTMLDivElement);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
navigator.vibrate?.(50); // 震动 50 毫秒
if (!isPc) {
PhoneContextShow.current = true;
}
setMessageContextMenuData({
left: e.clientX - 20,
top: e.clientY,
message
});
return false;
}, },
[isPc] [appId, loadHistory]
); );
// 获取对话信息 // get chat app info
const loadChatInfo = useCallback( const loadChatInfo = useCallback(
async ({ async ({
appId, appId,
chatId, historyId,
loading = false loading = false
}: { }: {
appId: string; appId: string;
chatId: string; historyId: string;
loading?: boolean; loading?: boolean;
}) => { }) => {
try { try {
loading && setIsLoading(true); loading && setIsLoading(true);
const res = await getInitChatSiteInfo(appId, chatId); const res = await getInitChatSiteInfo({ appId, historyId });
const history = res.history.map((item) => ({
...item,
status: 'finish' as any
}));
setChatData({ setChatData({
...res, ...res,
history: res.history.map((item) => ({ history
...item,
status: 'finish'
}))
}); });
// have records. // have records.
ChatBoxRef.current?.resetHistory(history);
ChatBoxRef.current?.resetVariables(res.variables);
if (res.history.length > 0) { if (res.history.length > 0) {
setTimeout(() => { setTimeout(() => {
scrollToBottom('auto'); ChatBoxRef.current?.scrollToBottom('auto');
}, 300); }, 200);
} }
// 空 modelId 请求, 重定向到新的 model 聊天 // empty appId request, return first app
if (res.modelId !== appId) { if (res.appId !== appId) {
setForbidLoadChatData(true); forbidRefresh.current = true;
router.replace(`/chat?appId=${res.modelId}`); router.replace({
query: {
appId: res.appId
}
});
} }
} catch (e: any) { } catch (e: any) {
// reset all chat tore // reset all chat tore
setLastChatModelId(''); setLastChatAppId('');
setLastChatId(''); setLastChatId('');
setChatData();
loadHistory({ pageNum: 1, init: true });
router.replace('/chat'); router.replace('/chat');
} }
setIsLoading(false); setIsLoading(false);
return null; return null;
}, },
[ [setIsLoading, setChatData, router, setLastChatAppId, setLastChatId]
setIsLoading,
setChatData,
scrollToBottom,
setForbidLoadChatData,
router,
setLastChatModelId,
setLastChatId,
loadHistory
]
); );
// 初始化聊天框 // 初始化聊天框
useQuery(['init', appId, chatId], () => { useQuery(['init', appId, historyId], () => {
// pc: redirect to latest model chat // pc: redirect to latest model chat
if (!appId && lastChatModelId) { if (!appId && lastChatAppId) {
router.replace(`/chat?appId=${lastChatModelId}&chatId=${lastChatId}`); router.replace({
query: {
appId: lastChatAppId,
historyId: lastChatId
}
});
return null; return null;
} }
// store id // store id
appId && setLastChatModelId(appId); appId && setLastChatAppId(appId);
setLastChatId(chatId); setLastChatId(historyId);
if (forbidLoadChatData) { if (forbidRefresh.current) {
setForbidLoadChatData(false); forbidRefresh.current = false;
return null; return null;
} }
return loadChatInfo({ return loadChatInfo({
appId, appId,
chatId, historyId,
loading: true loading: appId !== chatData.appId
}); });
}); });
// abort stream useQuery(['loadHistory', appId], () => (appId ? loadHistory({ appId }) : null));
useEffect(() => {
return () => {
window.speechSynthesis?.cancel();
isLeavePage.current = true;
controller.current?.abort();
};
}, [appId, chatId]);
// context menu component
const RenderContextMenu = useCallback(
({
history,
index,
AiDetail = false
}: {
history: ChatSiteItemType;
index: number;
AiDetail?: boolean;
}) => (
<MenuList fontSize={'sm'} minW={'100px !important'}>
<MenuItem onClick={() => onclickCopy(history.value)}>复制</MenuItem>
{AiDetail && chatData.model.canUse && history.obj === 'AI' && (
<MenuItem
borderBottom={theme.borders.base}
onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}
>
应用详情
</MenuItem>
)}
{hasVoiceApi && (
<MenuItem
borderBottom={theme.borders.base}
onClick={() => voiceBroadcast({ text: history.value })}
>
语音播报
</MenuItem>
)}
<MenuItem onClick={() => delChatRecord(index, history._id)}>删除</MenuItem>
</MenuList>
),
[
chatData.model.canUse,
chatData.modelId,
delChatRecord,
onclickCopy,
router,
theme.borders.base
]
);
return ( return (
<Flex <Flex h={'100%'} flexDirection={['column', 'row']}>
h={'100%'} {/* pc show myself apps */}
flexDirection={['column', 'row']} {isPc && (
backgroundColor={useColorModeValue('#fdfdfd', '')} <Box p={5} borderRight={theme.borders.base} w={'220px'} flexShrink={0}>
> <SliderApps appId={appId} />
{/* pc always show history. */} </Box>
{(isPc || !appId) && (
<SideBar>
<History onclickDelHistory={onclickDelHistory} onclickExportChat={onclickExportChat} />
</SideBar>
)} )}
{/* 聊天内容 */} <PageContainer flex={'1 0 0'} w={0} bg={'myWhite.600'} position={'relative'}>
{appId && ( <Flex h={'100%'} flexDirection={['column', 'row']}>
<Flex {/* pc always show history. */}
position={'relative'} {((children: React.ReactNode) => {
h={[0, '100%']} return isPc || !appId ? (
w={['100%', 0]} <SideBar>{children}</SideBar>
flex={'1 0 0'} ) : (
flexDirection={'column'} <Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
> <DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
{/* chat header */} <DrawerContent maxWidth={'250px'}>{children}</DrawerContent>
</Drawer>
);
})(
<ChatHistorySlider
appName={chatData.app.name}
appAvatar={chatData.app.avatar}
activeHistoryId={historyId}
history={history.map((item) => ({
id: item._id,
title: item.title,
top: item.top
}))}
onChangeChat={(historyId) => {
router.push({
query: {
historyId: historyId || '',
appId
}
});
}}
onDelHistory={delHistoryById}
onSetHistoryTop={async (e) => {
try {
await putChatHistory(e);
loadHistory({ appId });
} catch (error) {}
}}
onCloseSlider={onCloseSlider}
/>
)}
{/* chat container */}
<Flex <Flex
alignItems={'center'} position={'relative'}
justifyContent={'space-between'} h={[0, '100%']}
py={[3, 5]} w={['100%', 0]}
px={5} flex={'1 0 0'}
borderBottom={'1px solid'} flexDirection={'column'}
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
color={useColorModeValue('myGray.900', 'white')}
> >
{!isPc && ( <Flex
<MyIcon alignItems={'center'}
name={'menu'} py={[3, 5]}
w={'20px'} px={5}
h={'20px'} borderBottom={theme.borders.base}
color={useColorModeValue('blackAlpha.700', 'white')} borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
onClick={onOpenSlider} color={useColorModeValue('myGray.900', 'white')}
/>
)}
<Box
cursor={'pointer'}
lineHeight={1.2}
textAlign={'center'}
px={3}
fontSize={['sm', 'md']}
onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}
> >
{chatData.model.name} {ChatModelMap[chatData.chatModel]?.name} {isPc ? (
{chatData.history.length > 0 ? ` (${chatData.history.length})` : ''} <>
</Box> <Box mr={3} color={'myGray.1000'}>
{chatId ? ( {chatData.title}
<Menu autoSelect={false}> </Box>
<MenuButton lineHeight={1}> <Tag display={'flex'}>
<MyIcon name={'history'} w={'14px'} />
<Box ml={1}>{chatData.history.length}条记录</Box>
</Tag>
</>
) : (
<>
<MyIcon <MyIcon
name={'more'} name={'menu'}
w={'16px'} w={'20px'}
h={'16px'} h={'20px'}
color={useColorModeValue('blackAlpha.700', 'white')} color={useColorModeValue('blackAlpha.700', 'white')}
onClick={onOpenSlider}
/> />
</MenuButton> </>
<MenuList minW={`90px !important`}>
<MenuItem onClick={() => router.replace(`/chat?appId=${appId}`)}>新对话</MenuItem>
<MenuItem
onClick={async () => {
try {
setIsLoading(true);
await onclickDelHistory(chatData.chatId);
router.replace(`/chat?appId=${appId}`);
} catch (err) {
console.log(err);
}
setIsLoading(false);
}}
>
删除记录
</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={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'}>
<Box id={'history'}>
{chatData.history.map((item, index) => (
<Flex key={item._id} alignItems={'flex-start'} py={2} px={[2, 6, 8]}>
{item.obj === 'Human' && <Box flex={1} />}
{/* avatar */}
<Menu autoSelect={false} isLazy>
<MyTooltip label={item.obj === 'AI' ? '应用详情' : ''}>
<MenuButton
as={Box}
{...(item.obj === 'AI'
? {
order: 1,
mr: ['6px', 2],
cursor: 'pointer',
onClick: () =>
isPc &&
chatData.model.canUse &&
router.push(`/model?modelId=${chatData.modelId}`)
}
: {
order: 3,
ml: ['6px', 2]
})}
>
<Avatar
className="avatar"
src={
item.obj === 'Human'
? userInfo?.avatar || HUMAN_ICON
: chatData.model.avatar
}
w={['20px', '34px']}
h={['20px', '34px']}
/>
</MenuButton>
</MyTooltip>
{!isPc && <RenderContextMenu history={item} index={index} AiDetail />}
</Menu>
{/* message */}
<Flex order={2} pt={2} maxW={['calc(100% - 50px)', '80%']}>
{item.obj === 'AI' ? (
<Box w={'100%'}>
<Card
bg={'white'}
px={4}
py={3}
borderRadius={'0 8px 8px 8px'}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
/>
<Flex>
{!!item.systemPrompt && (
<Button
mt={2}
mr={3}
size={'xs'}
fontWeight={'normal'}
colorScheme={'gray'}
variant={'base'}
px={[2, 4]}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
>
提示词 & 限定词
</Button>
)}
{!!item.quoteLen && (
<Button
mt={2}
size={'xs'}
fontWeight={'normal'}
colorScheme={'gray'}
variant={'base'}
px={[2, 4]}
onClick={() => setShowHistoryQuote(item._id)}
>
{item.quoteLen}条引用
</Button>
)}
</Flex>
</Card>
</Box>
) : (
<Box>
<Card
className="markdown"
whiteSpace={'pre-wrap'}
px={4}
py={3}
borderRadius={'8px 0 8px 8px'}
bg={'myBlue.300'}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<Box as={'p'}>{item.value}</Box>
</Card>
</Box>
)}
</Flex>
</Flex>
))}
{chatData.history.length === 0 && (
<Empty model={chatData.model} showChatProblem={true} />
)} )}
</Flex>
{/* chat box */}
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appAvatar={chatData.app.avatar}
variableModules={chatData.app.variableModules}
welcomeText={chatData.app.welcomeText}
onUpdateVariable={(e) => {
console.log(e);
}}
onStartChat={startChat}
onDelMessage={delOneHistoryItem}
/>
</Box> </Box>
</Box> </Flex>
{/* 发送区 */}
{chatData.model.canUse ? (
<Box m={['0 auto', '20px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']}>
<Box
py={'18px'}
position={'relative'}
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
borderTop={['1px solid', 0]}
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
borderRadius={['none', 'md']}
backgroundColor={useColorModeValue('white', 'gray.700')}
>
{/* 输入框 */}
<Textarea
ref={TextareaDom}
py={0}
pr={['45px', '55px']}
border={'none'}
_focusVisible={{
border: 'none'
}}
placeholder="提问"
resize={'none'}
rows={1}
height={'22px'}
lineHeight={'22px'}
maxHeight={'150px'}
maxLength={-1}
overflowY={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
boxShadow={'none !important'}
color={useColorModeValue('blackAlpha.700', 'white')}
onChange={(e) => {
const textarea = e.target;
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
}}
onKeyDown={(e) => {
// 触发快捷发送
if (isPc && e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
// 全选内容
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
}}
/>
{/* 发送和等待按键 */}
<Flex
alignItems={'center'}
justifyContent={'center'}
h={'25px'}
w={'25px'}
position={'absolute'}
right={['12px', '20px']}
bottom={'15px'}
>
{isChatting ? (
<MyIcon
className={styles.stopIcon}
width={['22px', '25px']}
height={['22px', '25px']}
cursor={'pointer'}
name={'stop'}
color={useColorModeValue('gray.500', 'white')}
onClick={() => {
controller.current?.abort();
}}
/>
) : (
<MyIcon
name={'chatSend'}
width={['18px', '20px']}
height={['18px', '20px']}
cursor={'pointer'}
color={useColorModeValue('gray.500', 'white')}
onClick={sendPrompt}
/>
)}
</Flex>
</Box>
</Box>
) : (
<Box m={['0 auto', '20px auto']} w={'100%'} textAlign={'center'} color={'myGray.500'}>
作者已关闭分享
</Box>
)}
<Loading fixed={false} />
</Flex> </Flex>
)} <Loading fixed={false} />
</PageContainer>
{/* phone slider */}
{!isPc && (
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxW={'70%'}>
<PhoneSliderBar chatId={chatId} modelId={appId} onClose={onCloseSlider} />
</DrawerContent>
</Drawer>
)}
{/* quote modal*/} {/* quote modal*/}
{showHistoryQuote && chatId && ( {/* {showHistoryQuote && historyId && (
<QuoteModal <QuoteModal
historyId={showHistoryQuote} historyId={historyId}
chatId={chatId}
onClose={() => setShowHistoryQuote(undefined)} onClose={() => setShowHistoryQuote(undefined)}
/> />
)} )} */}
{/* system prompt show modal */} {/* system prompt show modal */}
{ {
<Modal isOpen={!!showSystemPrompt} onClose={() => setShowSystemPrompt('')}> <Modal isOpen={!!showSystemPrompt} onClose={() => setShowSystemPrompt('')}>
...@@ -924,26 +356,6 @@ const Chat = () => { ...@@ -924,26 +356,6 @@ const Chat = () => {
</ModalContent> </ModalContent>
</Modal> </Modal>
} }
{/* context menu */}
{messageContextMenuData && (
<Box
zIndex={10}
position={'fixed'}
top={messageContextMenuData.top}
left={messageContextMenuData.left}
>
<Box ref={ContextMenuRef}></Box>
<Menu isOpen>
<RenderContextMenu
history={messageContextMenuData.message}
index={chatData.history.findIndex(
(item) => item._id === messageContextMenuData.message._id
)}
AiDetail={!isPc}
/>
</Menu>
</Box>
)}
</Flex> </Flex>
); );
}; };
......
...@@ -18,13 +18,13 @@ import { streamFetch } from '@/api/fetch'; ...@@ -18,13 +18,13 @@ import { streamFetch } from '@/api/fetch';
import { useShareChatStore, defaultHistory } from '@/store/shareChat'; import { useShareChatStore, defaultHistory } from '@/store/shareChat';
import SideBar from '@/components/SideBar'; import SideBar from '@/components/SideBar';
import { gptMessage2ChatType } from '@/utils/adapt'; import { gptMessage2ChatType } from '@/utils/adapt';
import ChatHistorySlider from './components/ChatHistorySlider';
import { getErrText } from '@/utils/tools'; import { getErrText } from '@/utils/tools';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox'; import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import Tag from '@/components/Tag'; import Tag from '@/components/Tag';
import PageContainer from '@/components/PageContainer'; import PageContainer from '@/components/PageContainer';
import ChatHistorySlider from './components/ChatHistorySlider';
const ShareChat = () => { const ShareChat = () => {
const theme = useTheme(); const theme = useTheme();
...@@ -134,6 +134,10 @@ const ShareChat = () => { ...@@ -134,6 +134,10 @@ const ShareChat = () => {
} }
} }
if (history.chats.length > 0) {
ChatBoxRef.current?.scrollToBottom('auto');
}
return history; return history;
}, },
[ [
...@@ -152,7 +156,7 @@ const ShareChat = () => { ...@@ -152,7 +156,7 @@ const ShareChat = () => {
return ( return (
<PageContainer> <PageContainer>
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}> <Flex h={'100%'} flexDirection={['column', 'row']}>
{/* slider */} {/* slider */}
{isPc ? ( {isPc ? (
<SideBar> <SideBar>
...@@ -243,14 +247,7 @@ const ShareChat = () => { ...@@ -243,14 +247,7 @@ const ShareChat = () => {
)} )}
</Flex> </Flex>
{/* chat box */} {/* chat box */}
<Box <Box flex={1}>
pt={[0, 5]}
flex={1}
maxW={['100%', '1000px', '1200px']}
px={[0, 5]}
w={'100%'}
mx={'auto'}
>
<ChatBox <ChatBox
ref={ChatBoxRef} ref={ChatBoxRef}
appAvatar={shareChatData.app.avatar} appAvatar={shareChatData.app.avatar}
......
...@@ -18,17 +18,16 @@ const Login = () => { ...@@ -18,17 +18,16 @@ const Login = () => {
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login); const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo, setLastModelId, loadKbList, setLastKbId } = useUserStore(); const { setUserInfo, setLastModelId, loadKbList, setLastKbId } = useUserStore();
const { setLastChatId, setLastChatModelId, loadHistory } = useChatStore(); const { setLastChatId, setLastChatAppId } = useChatStore();
const loginSuccess = useCallback( const loginSuccess = useCallback(
(res: ResLogin) => { (res: ResLogin) => {
// init store // init store
setLastChatId(''); setLastChatId('');
setLastModelId(''); setLastModelId('');
setLastChatModelId(''); setLastChatAppId('');
setLastKbId(''); setLastKbId('');
loadKbList(true); loadKbList(true);
loadHistory({ pageNum: 1, init: true });
setUserInfo(res.user); setUserInfo(res.user);
setTimeout(() => { setTimeout(() => {
...@@ -37,11 +36,10 @@ const Login = () => { ...@@ -37,11 +36,10 @@ const Login = () => {
}, },
[ [
lastRoute, lastRoute,
loadHistory,
loadKbList, loadKbList,
router, router,
setLastChatId, setLastChatId,
setLastChatModelId, setLastChatAppId,
setLastKbId, setLastKbId,
setLastModelId, setLastModelId,
setUserInfo setUserInfo
......
...@@ -3,6 +3,7 @@ import { getErrText } from '@/utils/tools'; ...@@ -3,6 +3,7 @@ import { getErrText } from '@/utils/tools';
import { parseStreamChunk } from '@/utils/adapt'; import { parseStreamChunk } from '@/utils/adapt';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { sseResponse } from '../utils/tools'; import { sseResponse } from '../utils/tools';
import { SpecificInputEnum } from '@/constants/app';
interface Props { interface Props {
res: NextApiResponse; // 用于流转发 res: NextApiResponse; // 用于流转发
...@@ -13,7 +14,7 @@ export const moduleFetch = ({ url, data, res }: Props) => ...@@ -13,7 +14,7 @@ export const moduleFetch = ({ url, data, res }: Props) =>
new Promise<Record<string, any>>(async (resolve, reject) => { new Promise<Record<string, any>>(async (resolve, reject) => {
try { try {
const abortSignal = new AbortController(); const abortSignal = new AbortController();
const baseUrl = `http://localhost:3000/api`; const baseUrl = `http://localhost:${process.env.PORT || 3000}/api`;
const requestUrl = url.startsWith('/') ? `${baseUrl}${url}` : url; const requestUrl = url.startsWith('/') ? `${baseUrl}${url}` : url;
const response = await fetch(requestUrl, { const response = await fetch(requestUrl, {
method: 'POST', method: 'POST',
...@@ -41,7 +42,9 @@ export const moduleFetch = ({ url, data, res }: Props) => ...@@ -41,7 +42,9 @@ export const moduleFetch = ({ url, data, res }: Props) =>
const reader = response.body?.getReader(); const reader = response.body?.getReader();
let chatResponse: Record<string, any> = {}; let chatResponse: Record<string, any> = {
[SpecificInputEnum.answerText]: ''
};
const read = async () => { const read = async () => {
try { try {
...@@ -80,7 +83,8 @@ export const moduleFetch = ({ url, data, res }: Props) => ...@@ -80,7 +83,8 @@ export const moduleFetch = ({ url, data, res }: Props) =>
if (answer) { if (answer) {
chatResponse = { chatResponse = {
...chatResponse, ...chatResponse,
answer: chatResponse.answer ? chatResponse.answer + answer : answer [SpecificInputEnum.answerText]:
chatResponse[SpecificInputEnum.answerText] + answer
}; };
} }
......
...@@ -8,21 +8,11 @@ const ChatSchema = new Schema({ ...@@ -8,21 +8,11 @@ const ChatSchema = new Schema({
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
}, },
expiredTime: {
// 过期时间
type: Number,
default: () => new Date()
},
loadAmount: {
// 剩余加载次数
type: Number,
default: -1
},
updateTime: { updateTime: {
type: Date, type: Date,
default: () => new Date() default: () => new Date()
...@@ -35,13 +25,13 @@ const ChatSchema = new Schema({ ...@@ -35,13 +25,13 @@ const ChatSchema = new Schema({
type: String, type: String,
default: '' default: ''
}, },
latestChat: {
type: String,
default: ''
},
top: { top: {
type: Boolean type: Boolean
}, },
variables: {
type: Object,
default: {}
},
content: { content: {
type: [ type: [
{ {
......
...@@ -7,7 +7,7 @@ const CollectionSchema = new Schema({ ...@@ -7,7 +7,7 @@ const CollectionSchema = new Schema({
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
......
import { Schema, model, models, Model } from 'mongoose';
import { ChatSchema as ChatType } from '@/types/mongoSchema';
import { ChatRoleMap } from '@/constants/chat';
const InstallAppSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
modelId: {
type: Schema.Types.ObjectId,
ref: 'model',
required: true
}
});
export const InstallApp: Model<ChatType> = models['installApp'] || model('chat', InstallAppSchema);
...@@ -258,65 +258,6 @@ export const authKb = async ({ kbId, userId }: { kbId: string; userId: string }) ...@@ -258,65 +258,6 @@ export const authKb = async ({ kbId, userId }: { kbId: string; userId: string })
return Promise.reject(ERROR_ENUM.unAuthKb); return Promise.reject(ERROR_ENUM.unAuthKb);
}; };
// 获取对话校验
export const authChat = async ({
modelId,
chatId,
req
}: {
modelId: string;
chatId?: string;
req: NextApiRequest;
}) => {
const { userId } = await authUser({ req, authToken: true });
// 获取 app 数据
const { app, showModelDetail } = await authApp({
appId: modelId,
userId,
authOwner: false,
reserveDetail: true
});
// 聊天内容
let content: ChatItemType[] = [];
if (chatId) {
// 获取 chat 数据
content = await Chat.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(chatId) } },
{
$project: {
content: {
$slice: ['$content', -50] // 返回 content 数组的最后50个元素
}
}
},
{ $unwind: '$content' },
{
$project: {
obj: '$content.obj',
value: '$content.value',
quote: '$content.quote'
}
}
]);
}
// 获取 user 的 apiKey
const { userOpenAiKey, systemAuthKey } = await getApiKey({
model: app.chat.chatModel,
userId
});
return {
userOpenAiKey,
systemAuthKey,
content,
userId,
model: app,
showModelDetail
};
};
export const authShareChat = async ({ shareId }: { shareId: string }) => { export const authShareChat = async ({ shareId }: { shareId: string }) => {
// get shareChat // get shareChat
const shareChat = await ShareChat.findOne({ shareId }); const shareChat = await ShareChat.findOne({ shareId });
......
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { OpenAiChatEnum } from '@/constants/model';
import { ChatSiteItemType, HistoryItemType, ChatType } from '@/types/chat'; import { ChatHistoryItemType } from '@/types/chat';
import type { InitChatResponse } from '@/api/response/chat';
import { getChatHistory } from '@/api/chat'; import { getChatHistory } from '@/api/chat';
import { HUMAN_ICON } from '@/constants/chat'; import { HUMAN_ICON } from '@/constants/chat';
type SetShareChatHistoryItem = {
historyId: string;
shareId: string;
title: string;
latestChat: string;
chats: ChatSiteItemType[];
};
type State = { type State = {
history: HistoryItemType[]; history: ChatHistoryItemType[];
loadHistory: (data: { pageNum: number; init?: boolean }) => Promise<null>; loadHistory: (data: { appId?: string }) => Promise<null>;
forbidLoadChatData: boolean; updateHistory: (history: ChatHistoryItemType) => void;
setForbidLoadChatData: (val: boolean) => void; chatData: InitChatResponse;
chatData: ChatType; setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void;
setChatData: (e?: ChatType | ((e: ChatType) => ChatType)) => void; lastChatAppId: string;
lastChatModelId: string; setLastChatAppId: (id: string) => void;
setLastChatModelId: (id: string) => void;
lastChatId: string; lastChatId: string;
setLastChatId: (id: string) => void; setLastChatId: (id: string) => void;
}; };
const defaultChatData: ChatType = { const defaultChatData: InitChatResponse = {
chatId: 'chatId', historyId: '',
modelId: 'modelId', appId: '',
model: { app: {
name: '', name: '',
avatar: '/icon/logo.png', avatar: '/icon/logo.png',
intro: '', intro: '',
canUse: false canUse: false
}, },
chatModel: OpenAiChatEnum.GPT3516k, title: '新对话',
variables: {},
history: [] history: []
}; };
...@@ -45,10 +37,10 @@ export const useChatStore = create<State>()( ...@@ -45,10 +37,10 @@ export const useChatStore = create<State>()(
devtools( devtools(
persist( persist(
immer((set, get) => ({ immer((set, get) => ({
lastChatModelId: '', lastChatAppId: '',
setLastChatModelId(id: string) { setLastChatAppId(id: string) {
set((state) => { set((state) => {
state.lastChatModelId = id; state.lastChatAppId = id;
}); });
}, },
lastChatId: '', lastChatId: '',
...@@ -58,10 +50,12 @@ export const useChatStore = create<State>()( ...@@ -58,10 +50,12 @@ export const useChatStore = create<State>()(
}); });
}, },
history: [], history: [],
async loadHistory({ pageNum, init = false }: { pageNum: number; init?: boolean }) { async loadHistory({ appId }) {
if (get().history.length > 0 && !init) return null; const oneHistory = get().history[0];
if (oneHistory && oneHistory.appId === appId) return null;
const data = await getChatHistory({ const data = await getChatHistory({
pageNum, appId,
pageNum: 1,
pageSize: 20 pageSize: 20
}); });
set((state) => { set((state) => {
...@@ -69,14 +63,23 @@ export const useChatStore = create<State>()( ...@@ -69,14 +63,23 @@ export const useChatStore = create<State>()(
}); });
return null; return null;
}, },
forbidLoadChatData: false, updateHistory(history) {
setForbidLoadChatData(val: boolean) { const index = get().history.findIndex((item) => item._id === history._id);
set((state) => { set((state) => {
state.forbidLoadChatData = val; if (index > -1) {
const newHistory = [
history,
...get().history.slice(0, index),
...get().history.slice(index + 1)
];
state.history = newHistory;
} else {
state.history = [history, ...state.history];
}
}); });
}, },
chatData: defaultChatData, chatData: defaultChatData,
setChatData(e: ChatType | ((e: ChatType) => ChatType) = defaultChatData) { setChatData(e = defaultChatData) {
if (typeof e === 'function') { if (typeof e === 'function') {
set((state) => { set((state) => {
state.chatData = e(state.chatData); state.chatData = e(state.chatData);
...@@ -91,7 +94,7 @@ export const useChatStore = create<State>()( ...@@ -91,7 +94,7 @@ export const useChatStore = create<State>()(
{ {
name: 'chatStore', name: 'chatStore',
partialize: (state) => ({ partialize: (state) => ({
lastChatModelId: state.lastChatModelId, lastChatAppId: state.lastChatAppId,
lastChatId: state.lastChatId lastChatId: state.lastChatId
}) })
} }
......
...@@ -74,8 +74,7 @@ export const useUserStore = create<State>()( ...@@ -74,8 +74,7 @@ export const useUserStore = create<State>()(
async loadMyModels() { async loadMyModels() {
const res = await getMyModels(); const res = await getMyModels();
set((state) => { set((state) => {
state.myApps = res.myApps; state.myApps = res;
state.myCollectionApps = res.myCollectionApps;
}); });
return null; return null;
}, },
......
...@@ -18,10 +18,6 @@ export type ChatSiteItemType = { ...@@ -18,10 +18,6 @@ export type ChatSiteItemType = {
status: 'loading' | 'finish'; status: 'loading' | 'finish';
} & ChatItemType; } & ChatItemType;
export interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
export type HistoryItemType = { export type HistoryItemType = {
_id: string; _id: string;
updateTime: Date; updateTime: Date;
......
...@@ -58,7 +58,7 @@ export interface AppSchema { ...@@ -58,7 +58,7 @@ export interface AppSchema {
} }
export interface CollectionSchema { export interface CollectionSchema {
modelId: string; appId: string;
userId: string; userId: string;
} }
...@@ -78,19 +78,14 @@ export interface TrainingDataSchema { ...@@ -78,19 +78,14 @@ export interface TrainingDataSchema {
export interface ChatSchema { export interface ChatSchema {
_id: string; _id: string;
userId: string; userId: string;
modelId: string; appId: string;
expiredTime: number;
updateTime: Date; updateTime: Date;
title: string; title: string;
customTitle: string; customTitle: string;
latestChat: string;
top: boolean; top: boolean;
variables: Record<string, any>;
content: ChatItemType[]; content: ChatItemType[];
} }
export interface ChatPopulate extends ChatSchema {
userId: UserModelSchema;
modelId: AppSchema;
}
export interface BillSchema { export interface BillSchema {
_id: string; _id: string;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment