mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-24 22:03:54 +00:00
chatbox ui
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
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 type { AppListResponse } from './response/app';
|
||||
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
|
||||
/**
|
||||
* 根据 ID 删除模型
|
||||
*/
|
||||
export const delModelById = (id: string) => DELETE(`/app/del?modelId=${id}`);
|
||||
export const delModelById = (id: string) => DELETE(`/app/del?appId=${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 更新模型
|
||||
@@ -41,5 +40,5 @@ export const getShareModelList = (data: { searchText?: string } & RequestPaging)
|
||||
/**
|
||||
* 收藏/取消收藏模型
|
||||
*/
|
||||
export const triggerModelCollection = (modelId: string) =>
|
||||
POST<number>(`/app/share/collection?modelId=${modelId}`);
|
||||
export const triggerModelCollection = (appId: string) =>
|
||||
POST<number>(`/app/share/collection?appId=${appId}`);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { RequestPaging } from '../types/index';
|
||||
import type { ShareChatSchema } from '@/types/mongoSchema';
|
||||
@@ -11,14 +11,14 @@ import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updat
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
*/
|
||||
export const getInitChatSiteInfo = (modelId: '' | string, chatId: '' | string) =>
|
||||
GET<InitChatResponse>(`/chat/init?modelId=${modelId}&chatId=${chatId}`);
|
||||
export const getInitChatSiteInfo = (data: { appId: string; historyId?: string }) =>
|
||||
GET<InitChatResponse>(`/chat/init`, data);
|
||||
|
||||
/**
|
||||
* 获取历史记录
|
||||
*/
|
||||
export const getChatHistory = (data: RequestPaging) =>
|
||||
POST<HistoryItemType[]>('/chat/history/getHistory', data);
|
||||
export const getChatHistory = (data: RequestPaging & { appId?: string }) =>
|
||||
POST<ChatHistoryItemType[]>('/chat/history/getHistory', data);
|
||||
|
||||
/**
|
||||
* 删除一条历史记录
|
||||
@@ -44,8 +44,8 @@ export const updateHistoryQuote = (params: {
|
||||
/**
|
||||
* 删除一句对话
|
||||
*/
|
||||
export const delChatRecordByIndex = (chatId: string, contentId: string) =>
|
||||
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`);
|
||||
export const delChatRecordByIndex = (data: { historyId: string; contentId: string }) =>
|
||||
DELETE(`/chat/delChatRecordByContentId`, data);
|
||||
|
||||
/**
|
||||
* 修改历史记录: 标题/置顶
|
||||
|
@@ -9,12 +9,12 @@ interface StreamFetchProps {
|
||||
abortSignal: AbortController;
|
||||
}
|
||||
export const streamFetch = ({
|
||||
url = '/api/openapi/v1/chat/completions2',
|
||||
url = '/api/openapi/v1/chat/completions',
|
||||
data,
|
||||
onMessage,
|
||||
abortSignal
|
||||
}: StreamFetchProps) =>
|
||||
new Promise<{ responseText: string; errMsg: string; newChatId: string | null }>(
|
||||
new Promise<{ responseText: string; errMsg: string; newHistoryId: string | null }>(
|
||||
async (resolve, reject) => {
|
||||
try {
|
||||
const response = await window.fetch(url, {
|
||||
@@ -43,7 +43,7 @@ export const streamFetch = ({
|
||||
// response data
|
||||
let responseText = '';
|
||||
let errMsg = '';
|
||||
const newChatId = response.headers.get('newChatId');
|
||||
const newHistoryId = response.headers.get('newHistoryId');
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
@@ -53,7 +53,7 @@ export const streamFetch = ({
|
||||
return resolve({
|
||||
responseText,
|
||||
errMsg,
|
||||
newChatId
|
||||
newHistoryId
|
||||
});
|
||||
} else {
|
||||
return reject('响应过程出现异常~');
|
||||
@@ -85,7 +85,7 @@ export const streamFetch = ({
|
||||
return resolve({
|
||||
responseText,
|
||||
errMsg,
|
||||
newChatId
|
||||
newHistoryId
|
||||
});
|
||||
}
|
||||
reject(getErrText(err, '请求异常'));
|
||||
|
@@ -92,8 +92,8 @@ function request(url: string, data: any, config: ConfigType, method: Method): an
|
||||
baseURL: '/api',
|
||||
url,
|
||||
method,
|
||||
data: method === 'GET' ? null : data,
|
||||
params: method === 'GET' ? data : null, // get请求不携带data,params放在url上
|
||||
data: ['POST', 'PUT'].includes(method) ? data : null,
|
||||
params: !['POST', 'PUT'].includes(method) ? data : null,
|
||||
...config // 用户自定义配置,可以覆盖前面的配置
|
||||
})
|
||||
.then((res) => checkRes(res.data))
|
||||
@@ -119,6 +119,6 @@ export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise
|
||||
return request(url, data, config, 'PUT');
|
||||
}
|
||||
|
||||
export function DELETE<T>(url: string, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, {}, config, 'DELETE');
|
||||
export function DELETE<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'DELETE');
|
||||
}
|
||||
|
15
client/src/api/response/chat.d.ts
vendored
15
client/src/api/response/chat.d.ts
vendored
@@ -1,19 +1,20 @@
|
||||
import type { ChatPopulate, AppSchema } from '@/types/mongoSchema';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import { VariableItemType } from '@/types/app';
|
||||
|
||||
export interface InitChatResponse {
|
||||
chatId: string;
|
||||
modelId: string;
|
||||
systemPrompt?: string;
|
||||
limitPrompt?: string;
|
||||
model: {
|
||||
historyId: string;
|
||||
appId: string;
|
||||
app: {
|
||||
variableModules?: VariableItemType[];
|
||||
welcomeText?: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
intro: string;
|
||||
canUse: boolean;
|
||||
};
|
||||
chatModel: AppSchema['chat']['chatModel']; // 对话模型名
|
||||
title: string;
|
||||
variables: Record<string, any>;
|
||||
history: ChatItemType[];
|
||||
}
|
||||
|
||||
|
@@ -38,8 +38,10 @@ export type StartChatFnProps = {
|
||||
};
|
||||
|
||||
export type ComponentRef = {
|
||||
getChatHistory: () => ChatSiteItemType[];
|
||||
resetVariables: (data?: Record<string, any>) => void;
|
||||
resetHistory: (history: ChatSiteItemType[]) => void;
|
||||
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
|
||||
};
|
||||
|
||||
const VariableLabel = ({
|
||||
@@ -73,7 +75,7 @@ const ChatBox = (
|
||||
welcomeText?: string;
|
||||
onUpdateVariable?: (e: Record<string, any>) => void;
|
||||
onStartChat: (e: StartChatFnProps) => Promise<{ responseText: string }>;
|
||||
onDelMessage?: (e: { id?: string; index: number }) => void;
|
||||
onDelMessage?: (e: { contentId?: string; index: number }) => void;
|
||||
},
|
||||
ref: ForwardedRef<ComponentRef>
|
||||
) => {
|
||||
@@ -279,6 +281,7 @@ const ChatBox = (
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getChatHistory: () => chatHistory,
|
||||
resetVariables(e) {
|
||||
const defaultVal: Record<string, any> = {};
|
||||
variableModules?.forEach((item) => {
|
||||
@@ -290,7 +293,8 @@ const ChatBox = (
|
||||
},
|
||||
resetHistory(e) {
|
||||
setChatHistory(e);
|
||||
}
|
||||
},
|
||||
scrollToBottom
|
||||
}));
|
||||
|
||||
const controlIconStyle = {
|
||||
@@ -305,210 +309,215 @@ const ChatBox = (
|
||||
};
|
||||
const controlContainerStyle = {
|
||||
className: 'control',
|
||||
display: ['flex', 'none'],
|
||||
display: isChatting ? 'none' : ['flex', 'none'],
|
||||
color: 'myGray.400',
|
||||
pl: 1,
|
||||
mt: 2,
|
||||
position: 'absolute' as any,
|
||||
zIndex: 1
|
||||
zIndex: 1,
|
||||
w: '100%'
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Box ref={ChatBoxRef} flex={'1 0 0'} overflow={'overlay'} px={[2, 5]}>
|
||||
{/* variable input */}
|
||||
{(variableModules || welcomeText) && (
|
||||
<Flex alignItems={'flex-start'} py={2}>
|
||||
{/* avatar */}
|
||||
<Avatar
|
||||
src={appAvatar}
|
||||
w={isLargeWidth ? '34px' : '24px'}
|
||||
h={isLargeWidth ? '34px' : '24px'}
|
||||
order={1}
|
||||
mr={['6px', 2]}
|
||||
/>
|
||||
{/* message */}
|
||||
<Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||
{welcomeText && (
|
||||
<Box mb={2} pb={2} borderBottom={theme.borders.base}>
|
||||
{welcomeText}
|
||||
</Box>
|
||||
)}
|
||||
{variableModules && (
|
||||
<Box>
|
||||
{variableModules.map((item) => (
|
||||
<Box w={'min(100%,300px)'} key={item.id} mb={4}>
|
||||
<VariableLabel required={item.required}>{item.label}</VariableLabel>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
onchange={(e) => {
|
||||
setValue(item.key, e);
|
||||
// setRefresh((state) => !state);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
borderRadius={'lg'}
|
||||
onClick={handleSubmit((data) => {
|
||||
onUpdateVariable?.(data);
|
||||
setVariables(data);
|
||||
})}
|
||||
>
|
||||
{'开始对话'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{/* chat history */}
|
||||
<Box id={'history'}>
|
||||
{chatHistory.map((item, index) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'flex-start'}
|
||||
py={2}
|
||||
_hover={{
|
||||
'& .control': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.obj === 'Human' && <Box flex={1} />}
|
||||
<Box ref={ChatBoxRef} flex={'1 0 0'} overflow={'overlay'} px={[2, 5, 8]} py={5}>
|
||||
<Box maxW={['100%', '1000px', '1200px']} mx={'auto'}>
|
||||
{/* variable input */}
|
||||
{(variableModules || welcomeText) && (
|
||||
<Flex alignItems={'flex-start'} py={2}>
|
||||
{/* avatar */}
|
||||
<Avatar
|
||||
src={item.obj === 'Human' ? userInfo?.avatar || HUMAN_ICON : appAvatar}
|
||||
w={isLargeWidth ? '34px' : '24px'}
|
||||
h={isLargeWidth ? '34px' : '24px'}
|
||||
{...(item.obj === 'AI'
|
||||
? {
|
||||
order: 1,
|
||||
mr: ['6px', 2]
|
||||
}
|
||||
: {
|
||||
order: 3,
|
||||
ml: ['6px', 2]
|
||||
})}
|
||||
src={appAvatar}
|
||||
w={['24px', '34px']}
|
||||
h={['24px', '34px']}
|
||||
order={1}
|
||||
mr={['6px', 2]}
|
||||
/>
|
||||
{/* message */}
|
||||
<Box order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||
{item.obj === 'AI' ? (
|
||||
<Box w={'100%'}>
|
||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||
<Markdown
|
||||
source={item.value}
|
||||
isChatting={index === chatHistory.length - 1 && isChatting}
|
||||
/>
|
||||
</Card>
|
||||
<Flex {...controlContainerStyle}>
|
||||
<MyTooltip label={'复制'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'myBlue.700' }}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{onDelMessage && (
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
setChatHistory((state) =>
|
||||
state.filter((chat) => chat._id !== item._id)
|
||||
);
|
||||
onDelMessage({
|
||||
id: item._id,
|
||||
index
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<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
|
||||
isDisabled={variableIsFinish}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
isDisabled={variableIsFinish}
|
||||
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>
|
||||
)}
|
||||
{hasVoiceApi && (
|
||||
<MyTooltip label={'语音播报'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'voice'}
|
||||
_hover={{ color: '#E74694' }}
|
||||
onClick={() => voiceBroadcast({ text: item.value })}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<Box position={'relative'}>
|
||||
<Card
|
||||
className="markdown"
|
||||
whiteSpace={'pre-wrap'}
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'8px 0 8px 8px'}
|
||||
bg={'myBlue.300'}
|
||||
>
|
||||
<Box as={'p'}>{item.value}</Box>
|
||||
</Card>
|
||||
<Flex {...controlContainerStyle} right={0}>
|
||||
<MyTooltip label={'复制'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'myBlue.700' }}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{onDelMessage && (
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
mr={0}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
setChatHistory((state) =>
|
||||
state.filter((chat) => chat._id !== item._id)
|
||||
);
|
||||
onDelMessage({
|
||||
id: item._id,
|
||||
index
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</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
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'myBlue.700' }}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{onDelMessage && (
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
setChatHistory((state) =>
|
||||
state.filter((chat) => chat._id !== item._id)
|
||||
);
|
||||
onDelMessage({
|
||||
contentId: item._id,
|
||||
index
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{hasVoiceApi && (
|
||||
<MyTooltip label={'语音播报'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'voice'}
|
||||
_hover={{ color: '#E74694' }}
|
||||
onClick={() => voiceBroadcast({ text: item.value })}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<Box position={'relative'}>
|
||||
<Card
|
||||
className="markdown"
|
||||
whiteSpace={'pre-wrap'}
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'8px 0 8px 8px'}
|
||||
bg={'myBlue.300'}
|
||||
>
|
||||
<Box as={'p'}>{item.value}</Box>
|
||||
</Card>
|
||||
<Flex {...controlContainerStyle} right={0}>
|
||||
<MyTooltip label={'复制'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'myBlue.700' }}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{onDelMessage && (
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
mr={0}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
setChatHistory((state) =>
|
||||
state.filter((chat) => chat._id !== item._id)
|
||||
);
|
||||
onDelMessage({
|
||||
contentId: item._id,
|
||||
index
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{variableIsFinish ? (
|
||||
@@ -531,7 +540,6 @@ const ChatBox = (
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
isDisabled={isChatting}
|
||||
placeholder="提问"
|
||||
resize={'none'}
|
||||
rows={1}
|
||||
|
@@ -15,7 +15,8 @@ const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/': true,
|
||||
'/login': true,
|
||||
'/chat/share': true,
|
||||
'/app/edit': true
|
||||
'/app/edit': true,
|
||||
'/chat': true
|
||||
};
|
||||
const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/': true,
|
||||
|
@@ -17,14 +17,14 @@ export enum NavbarTypeEnum {
|
||||
const Navbar = ({ unread }: { unread: number }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, lastModelId } = useUserStore();
|
||||
const { lastChatModelId, lastChatId } = useChatStore();
|
||||
const { lastChatAppId, lastChatId } = useChatStore();
|
||||
const navbarList = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: '聊天',
|
||||
icon: 'chatLight',
|
||||
activeIcon: 'chatFill',
|
||||
link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`,
|
||||
link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`,
|
||||
activeLink: ['/chat']
|
||||
},
|
||||
{
|
||||
@@ -56,7 +56,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
activeLink: ['/number']
|
||||
}
|
||||
],
|
||||
[lastChatId, lastChatModelId]
|
||||
[lastChatId, lastChatAppId]
|
||||
);
|
||||
|
||||
const itemStyles: any = {
|
||||
|
@@ -7,13 +7,13 @@ import Badge from '../Badge';
|
||||
|
||||
const NavbarPhone = ({ unread }: { unread: number }) => {
|
||||
const router = useRouter();
|
||||
const { lastChatModelId, lastChatId } = useChatStore();
|
||||
const { lastChatAppId, lastChatId } = useChatStore();
|
||||
const navbarList = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: '聊天',
|
||||
icon: 'tabbarChat',
|
||||
link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`,
|
||||
link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`,
|
||||
activeLink: ['/chat'],
|
||||
unread: 0
|
||||
},
|
||||
@@ -39,7 +39,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
|
||||
unread
|
||||
}
|
||||
],
|
||||
[lastChatId, lastChatModelId, unread]
|
||||
[lastChatId, lastChatAppId, unread]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -7,7 +7,7 @@ interface Props extends BoxProps {}
|
||||
|
||||
const SideBar = (e?: Props) => {
|
||||
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,
|
||||
...props
|
||||
} = e || {};
|
||||
|
@@ -10,7 +10,7 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
|
||||
const theme = useMemo(() => {
|
||||
const map = {
|
||||
blue: {
|
||||
borderColor: 'myBlue.700',
|
||||
borderColor: 'myBlue.600',
|
||||
bg: '#F2FBFF',
|
||||
color: 'myBlue.700'
|
||||
},
|
||||
|
@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth';
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
const { appId } = req.query as { appId: string };
|
||||
|
||||
if (!modelId) {
|
||||
if (!appId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
|
||||
@@ -20,28 +20,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
// 验证是否是该用户的 model
|
||||
await authApp({
|
||||
appId: modelId,
|
||||
appId,
|
||||
userId
|
||||
});
|
||||
|
||||
// 删除对应的聊天
|
||||
await Chat.deleteMany({
|
||||
modelId
|
||||
appId
|
||||
});
|
||||
|
||||
// 删除收藏列表
|
||||
await Collection.deleteMany({
|
||||
modelId
|
||||
modelId: appId
|
||||
});
|
||||
|
||||
// 删除分享链接
|
||||
await ShareChat.deleteMany({
|
||||
modelId
|
||||
appId
|
||||
});
|
||||
|
||||
// 删除模型
|
||||
await App.deleteOne({
|
||||
_id: modelId,
|
||||
_id: appId,
|
||||
userId
|
||||
});
|
||||
|
||||
|
@@ -7,9 +7,9 @@ import { authApp } from '@/service/utils/auth';
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
const { appId } = req.query as { appId: string };
|
||||
|
||||
if (!modelId) {
|
||||
if (!appId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
await connectToDatabase();
|
||||
|
||||
const { app } = await authApp({
|
||||
appId: modelId,
|
||||
appId,
|
||||
userId,
|
||||
authOwner: false
|
||||
});
|
||||
|
@@ -1,58 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Collection, App } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { AppListResponse } from '@/api/response/app';
|
||||
|
||||
/* 获取模型列表 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 userId 获取模型信息
|
||||
const [myApps, myCollections] = await Promise.all([
|
||||
App.find(
|
||||
{
|
||||
userId
|
||||
},
|
||||
'_id avatar name intro'
|
||||
).sort({
|
||||
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, {
|
||||
data: {
|
||||
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) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
33
client/src/pages/api/app/myApps.ts
Normal file
33
client/src/pages/api/app/myApps.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, App } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { AppListItemType } from '@/types/app';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 userId 获取模型信息
|
||||
const myApps = await App.find(
|
||||
{
|
||||
userId
|
||||
},
|
||||
'_id avatar name intro'
|
||||
).sort({
|
||||
updateTime: -1
|
||||
});
|
||||
|
||||
jsonRes<AppListItemType[]>(res, {
|
||||
data: myApps
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
@@ -6,9 +6,9 @@ import { authUser } from '@/service/utils/auth';
|
||||
/* 模型收藏切换 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
const { appId } = req.query as { appId: string };
|
||||
|
||||
if (!modelId) {
|
||||
if (!appId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
// 凭证校验
|
||||
@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
const collectionRecord = await Collection.findOne({
|
||||
userId,
|
||||
modelId
|
||||
modelId: appId
|
||||
});
|
||||
|
||||
if (collectionRecord) {
|
||||
@@ -26,12 +26,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
} else {
|
||||
await Collection.create({
|
||||
userId,
|
||||
modelId
|
||||
modelId: appId
|
||||
});
|
||||
}
|
||||
|
||||
await App.findByIdAndUpdate(modelId, {
|
||||
'share.collection': await Collection.countDocuments({ modelId })
|
||||
await App.findByIdAndUpdate(appId, {
|
||||
'share.collection': await Collection.countDocuments({ modelId: appId })
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
|
@@ -6,14 +6,15 @@ import { sseResponseEventEnum } from '@/constants/chat';
|
||||
import { sseResponse } from '@/service/utils/tools';
|
||||
import { type ChatCompletionRequestMessage } from 'openai';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { dispatchModules } from '../openapi/v1/chat/completions2';
|
||||
import { dispatchModules } from '../openapi/v1/chat/completions';
|
||||
import { gptMessage2ChatType } from '@/utils/adapt';
|
||||
|
||||
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
|
||||
export type Props = {
|
||||
history: MessageItemType[];
|
||||
prompt: string;
|
||||
modules: AppModuleItemType[];
|
||||
variable: Record<string, any>;
|
||||
variables: Record<string, any>;
|
||||
};
|
||||
export type ChatResponseType = {
|
||||
newChatId: string;
|
||||
@@ -29,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
res.end();
|
||||
});
|
||||
|
||||
let { modules = [], history = [], prompt, variable = {} } = req.body as Props;
|
||||
let { modules = [], history = [], prompt, variables = {} } = req.body as Props;
|
||||
|
||||
try {
|
||||
if (!history || !modules || !prompt) {
|
||||
@@ -48,9 +49,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { responseData } = await dispatchModules({
|
||||
res,
|
||||
modules: modules,
|
||||
variable,
|
||||
variables,
|
||||
params: {
|
||||
history,
|
||||
history: gptMessage2ChatType(history),
|
||||
userChatInput: prompt
|
||||
},
|
||||
stream: true
|
||||
|
@@ -5,12 +5,10 @@ import { authUser } from '@/service/utils/auth';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, contentId } = req.query as {
|
||||
chatId: string;
|
||||
contentId: string;
|
||||
};
|
||||
const { historyId, contentId } = req.query as { historyId: string; contentId: string };
|
||||
console.log(historyId, contentId);
|
||||
|
||||
if (!chatId || !contentId) {
|
||||
if (!historyId || !contentId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
@@ -19,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const chatRecord = await Chat.findById(chatId);
|
||||
const chatRecord = await Chat.findById(historyId);
|
||||
|
||||
if (!chatRecord) {
|
||||
throw new Error('找不到对话');
|
||||
@@ -28,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 删除一条数据库记录
|
||||
await Chat.updateOne(
|
||||
{
|
||||
_id: chatId,
|
||||
_id: historyId,
|
||||
userId
|
||||
},
|
||||
{ $pull: { content: { _id: contentId } } }
|
||||
|
@@ -2,31 +2,32 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
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) {
|
||||
try {
|
||||
const { appId } = req.body as { appId?: string };
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
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 })
|
||||
.limit(20);
|
||||
|
||||
jsonRes<HistoryItemType[]>(res, {
|
||||
jsonRes<ChatHistoryItemType[]>(res, {
|
||||
data: data.map((item) => ({
|
||||
_id: item._id,
|
||||
updateTime: item.updateTime,
|
||||
modelId: item.modelId,
|
||||
appId: item.appId,
|
||||
title: item.customTitle || item.title,
|
||||
latestChat: item.latestChat,
|
||||
top: item.top
|
||||
}))
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
|
||||
export type Props = {
|
||||
chatId: '' | string;
|
||||
historyId: string;
|
||||
customTitle?: string;
|
||||
top?: boolean;
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export type Props = {
|
||||
/* 更新聊天标题 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, customTitle, top } = req.body as Props;
|
||||
const { historyId, customTitle, top } = req.body as Props;
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
await Chat.findOneAndUpdate(
|
||||
{
|
||||
_id: chatId,
|
||||
_id: historyId,
|
||||
userId
|
||||
},
|
||||
{
|
||||
|
@@ -6,23 +6,25 @@ import { authUser } from '@/service/utils/auth';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
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) {
|
||||
try {
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
let { modelId, chatId } = req.query as {
|
||||
modelId: '' | string;
|
||||
chatId: '' | string;
|
||||
let { appId, historyId } = req.query as {
|
||||
appId: '' | string;
|
||||
historyId: '' | string;
|
||||
};
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 没有 modelId 时,直接获取用户的第一个id
|
||||
// 没有 appId 时,直接获取用户的第一个id
|
||||
const app = await (async () => {
|
||||
if (!modelId) {
|
||||
if (!appId) {
|
||||
const myModel = await App.findOne({ userId });
|
||||
if (!myModel) {
|
||||
const { _id } = await App.create({
|
||||
@@ -36,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
} else {
|
||||
// 校验使用权限
|
||||
const authRes = await authApp({
|
||||
appId: modelId,
|
||||
appId,
|
||||
userId,
|
||||
authUser: false,
|
||||
authOwner: false
|
||||
@@ -45,63 +47,71 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
})();
|
||||
|
||||
modelId = modelId || app._id;
|
||||
appId = appId || app._id;
|
||||
|
||||
// 历史记录
|
||||
let history: ChatItemType[] = [];
|
||||
|
||||
if (chatId) {
|
||||
// auth chatId
|
||||
const chat = await Chat.countDocuments({
|
||||
_id: chatId,
|
||||
userId
|
||||
});
|
||||
if (chat === 0) {
|
||||
throw new Error('聊天框不存在');
|
||||
}
|
||||
// 获取 chat.content 数据
|
||||
history = await Chat.aggregate([
|
||||
{
|
||||
$match: {
|
||||
_id: new mongoose.Types.ObjectId(chatId),
|
||||
userId: new mongoose.Types.ObjectId(userId)
|
||||
const { chat, history = [] }: { chat?: ChatSchema; history?: ChatItemType[] } =
|
||||
await (async () => {
|
||||
if (historyId) {
|
||||
// auth chatId
|
||||
const chat = await Chat.findOne({
|
||||
_id: historyId,
|
||||
userId
|
||||
});
|
||||
if (!chat) {
|
||||
throw new Error('聊天框不存在');
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
content: {
|
||||
$slice: ['$content', -50] // 返回 content 数组的最后50个元素
|
||||
// 获取 chat.content 数据
|
||||
const history = await Chat.aggregate([
|
||||
{
|
||||
$match: {
|
||||
_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', []] } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $unwind: '$content' },
|
||||
{
|
||||
$project: {
|
||||
_id: '$content._id',
|
||||
obj: '$content.obj',
|
||||
value: '$content.value',
|
||||
systemPrompt: '$content.systemPrompt',
|
||||
quoteLen: { $size: { $ifNull: ['$content.quote', []] } }
|
||||
}
|
||||
]);
|
||||
return { history, chat };
|
||||
}
|
||||
]);
|
||||
}
|
||||
return {};
|
||||
})();
|
||||
|
||||
const isOwner = String(app.userId) === userId;
|
||||
|
||||
jsonRes<InitChatResponse>(res, {
|
||||
data: {
|
||||
chatId: chatId || '',
|
||||
modelId: modelId,
|
||||
model: {
|
||||
historyId,
|
||||
appId,
|
||||
app: {
|
||||
variableModules: app.modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs?.find((item) => item.key === SystemInputEnum.variables)?.value,
|
||||
welcomeText: app.modules
|
||||
.find((item) => item.flowType === FlowModuleTypeEnum.userGuide)
|
||||
?.inputs?.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro,
|
||||
canUse: app.share.isShare || isOwner
|
||||
},
|
||||
chatModel: app.chat.chatModel,
|
||||
systemPrompt: isOwner ? app.chat.systemPrompt : '',
|
||||
limitPrompt: isOwner ? app.chat.limitPrompt : '',
|
||||
title: chat?.title || '新对话',
|
||||
variables: chat?.variables || {},
|
||||
history
|
||||
}
|
||||
});
|
||||
|
@@ -7,15 +7,16 @@ import { authUser } from '@/service/utils/auth';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
type Props = {
|
||||
chatId?: string;
|
||||
modelId: string;
|
||||
historyId?: string;
|
||||
appId: string;
|
||||
variables?: Record<string, any>;
|
||||
prompts: [ChatItemType, ChatItemType];
|
||||
};
|
||||
|
||||
/* 聊天内容存存储 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, modelId, prompts } = req.body as Props;
|
||||
const { historyId, appId, prompts } = req.body as Props;
|
||||
|
||||
if (!prompts) {
|
||||
throw new Error('缺少参数');
|
||||
@@ -24,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const response = await saveChat({
|
||||
chatId,
|
||||
modelId,
|
||||
historyId,
|
||||
appId,
|
||||
prompts,
|
||||
userId
|
||||
});
|
||||
@@ -42,14 +43,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
export async function saveChat({
|
||||
newChatId,
|
||||
chatId,
|
||||
modelId,
|
||||
newHistoryId,
|
||||
historyId,
|
||||
appId,
|
||||
prompts,
|
||||
variables,
|
||||
userId
|
||||
}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> {
|
||||
}: Props & { newHistoryId?: Types.ObjectId; userId: string }): Promise<{ newHistoryId: string }> {
|
||||
await connectToDatabase();
|
||||
const { app } = await authApp({ appId: modelId, userId, authOwner: false });
|
||||
const { app } = await authApp({ appId, userId, authOwner: false });
|
||||
|
||||
const content = prompts.map((item) => ({
|
||||
_id: item._id,
|
||||
@@ -60,43 +62,45 @@ export async function saveChat({
|
||||
}));
|
||||
|
||||
if (String(app.userId) === userId) {
|
||||
await App.findByIdAndUpdate(modelId, {
|
||||
await App.findByIdAndUpdate(appId, {
|
||||
updateTime: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const [response] = await Promise.all([
|
||||
...(chatId
|
||||
...(historyId
|
||||
? [
|
||||
Chat.findByIdAndUpdate(chatId, {
|
||||
Chat.findByIdAndUpdate(historyId, {
|
||||
$push: {
|
||||
content: {
|
||||
$each: content
|
||||
}
|
||||
},
|
||||
variables,
|
||||
title: content[0].value.slice(0, 20),
|
||||
latestChat: content[1].value,
|
||||
updateTime: new Date()
|
||||
}).then(() => ({
|
||||
newChatId: ''
|
||||
newHistoryId: ''
|
||||
}))
|
||||
]
|
||||
: [
|
||||
Chat.create({
|
||||
_id: newChatId,
|
||||
_id: newHistoryId,
|
||||
userId,
|
||||
modelId,
|
||||
appId,
|
||||
variables,
|
||||
content,
|
||||
title: content[0].value.slice(0, 20),
|
||||
latestChat: content[1].value
|
||||
}).then((res) => ({
|
||||
newChatId: String(res._id)
|
||||
newHistoryId: String(res._id)
|
||||
}))
|
||||
]),
|
||||
// update app
|
||||
...(String(app.userId) === userId
|
||||
? [
|
||||
App.findByIdAndUpdate(modelId, {
|
||||
App.findByIdAndUpdate(appId, {
|
||||
updateTime: new Date()
|
||||
})
|
||||
]
|
||||
@@ -105,6 +109,6 @@ export async function saveChat({
|
||||
|
||||
return {
|
||||
// @ts-ignore
|
||||
newChatId: response?.newChatId || ''
|
||||
newHistoryId: response?.newHistoryId || ''
|
||||
};
|
||||
}
|
||||
|
@@ -1,189 +0,0 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
@@ -1,43 +1,41 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatModelMap } from '@/constants/model';
|
||||
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
|
||||
import { authUser, authApp, authShareChat } from '@/service/utils/auth';
|
||||
import { sseErrRes, jsonRes } from '@/service/response';
|
||||
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { BillTypeEnum } from '@/constants/user';
|
||||
import { appKbSearch } from '../../../openapi/kb/appKbSearch';
|
||||
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 { 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 };
|
||||
type FastGptWebChatProps = {
|
||||
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
|
||||
historyId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
|
||||
appId?: string;
|
||||
};
|
||||
type FastGptShareChatProps = {
|
||||
password?: string;
|
||||
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();
|
||||
@@ -47,8 +45,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
res.end();
|
||||
});
|
||||
|
||||
let { chatId, appId, shareId, password = '', stream = false, messages = [] } = req.body as Props;
|
||||
let step = 0;
|
||||
let {
|
||||
historyId,
|
||||
appId,
|
||||
shareId,
|
||||
stream = false,
|
||||
messages = [],
|
||||
variables = {}
|
||||
} = req.body as Props;
|
||||
|
||||
try {
|
||||
if (!messages) {
|
||||
@@ -68,8 +72,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
authType
|
||||
} = await (shareId
|
||||
? authShareChat({
|
||||
shareId,
|
||||
password
|
||||
shareId
|
||||
})
|
||||
: authUser({ req }));
|
||||
|
||||
@@ -78,257 +81,96 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
throw new Error('appId is empty');
|
||||
}
|
||||
|
||||
// auth app permission
|
||||
const { app, showModelDetail } = await authApp({
|
||||
userId,
|
||||
appId,
|
||||
authOwner: false,
|
||||
reserveDetail: true
|
||||
});
|
||||
// auth app, get history
|
||||
const [{ app }, { history }] = await Promise.all([
|
||||
authApp({
|
||||
appId,
|
||||
userId
|
||||
}),
|
||||
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));
|
||||
// adapt fastgpt web
|
||||
if (prompts[prompts.length - 1].obj === 'AI') {
|
||||
prompts.pop();
|
||||
}
|
||||
// user question
|
||||
const prompt = prompts[prompts.length - 1];
|
||||
const prompt = prompts.pop();
|
||||
|
||||
const {
|
||||
rawSearch = [],
|
||||
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 }
|
||||
]
|
||||
});
|
||||
}
|
||||
if (!prompt) {
|
||||
throw new Error('Question is empty');
|
||||
}
|
||||
|
||||
// api messages. [quote,context,systemPrompt,question]
|
||||
const completePrompts = [
|
||||
...quotePrompt,
|
||||
...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
|
||||
);
|
||||
const newHistoryId = historyId === '' ? new Types.ObjectId() : undefined;
|
||||
if (stream && newHistoryId) {
|
||||
res.setHeader('newHistoryId', String(newHistoryId));
|
||||
}
|
||||
|
||||
await sensitiveCheck({
|
||||
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
|
||||
/* start process */
|
||||
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
|
||||
const { streamResponse, responseMessages, responseText, totalTokens } =
|
||||
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') {
|
||||
// save chat
|
||||
if (typeof historyId === 'string') {
|
||||
await saveChat({
|
||||
newChatId,
|
||||
chatId,
|
||||
modelId: appId,
|
||||
historyId,
|
||||
newHistoryId,
|
||||
appId,
|
||||
prompts: [
|
||||
prompt,
|
||||
{
|
||||
_id: messages[messages.length - 1]._id,
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: answer,
|
||||
...(showAppDetail
|
||||
? {
|
||||
quote: rawSearch,
|
||||
systemPrompt: `${userSystemPrompt[0]?.value}\n\n${userLimitPrompt[0]?.value}`
|
||||
}
|
||||
: {})
|
||||
value: answerText,
|
||||
responseData
|
||||
}
|
||||
],
|
||||
userId
|
||||
});
|
||||
}
|
||||
|
||||
// close response
|
||||
if (stream) {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: '[DONE]'
|
||||
});
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.appStreamResponse,
|
||||
data: JSON.stringify(responseData)
|
||||
});
|
||||
res.end();
|
||||
} else {
|
||||
res.json({
|
||||
...(showAppDetail
|
||||
? {
|
||||
rawSearch
|
||||
}
|
||||
: {}),
|
||||
newChatId,
|
||||
id: chatId || '',
|
||||
object: 'chat.completion',
|
||||
created: 1688608930,
|
||||
model: app.chat.chatModel,
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
|
||||
data: {
|
||||
newHistoryId,
|
||||
...responseData
|
||||
},
|
||||
id: historyId || '',
|
||||
model: '',
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
||||
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) {
|
||||
res.status(500);
|
||||
if (step === 1) {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.error,
|
||||
data: JSON.stringify(err)
|
||||
});
|
||||
if (stream) {
|
||||
res.status(500);
|
||||
sseErrRes(res, err);
|
||||
res.end();
|
||||
} else {
|
||||
jsonRes(res, {
|
||||
@@ -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 };
|
||||
}
|
||||
|
@@ -1,405 +0,0 @@
|
||||
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';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
|
||||
export type Props = {
|
||||
chatId?: string;
|
||||
historyId?: string;
|
||||
limit?: number;
|
||||
};
|
||||
export type Response = { history: ChatItemType[] };
|
||||
@@ -16,11 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { userId } = await authUser({ req });
|
||||
const { chatId, limit } = req.body as Props;
|
||||
const { historyId, limit } = req.body as Props;
|
||||
|
||||
jsonRes<Response>(res, {
|
||||
data: await getChatHistory({
|
||||
chatId,
|
||||
historyId,
|
||||
userId,
|
||||
limit
|
||||
})
|
||||
@@ -34,16 +34,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
export async function getChatHistory({
|
||||
chatId,
|
||||
historyId,
|
||||
userId,
|
||||
limit = 50
|
||||
}: Props & { userId: string }): Promise<Response> {
|
||||
if (!chatId) {
|
||||
if (!historyId) {
|
||||
return { history: [] };
|
||||
}
|
||||
|
||||
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: {
|
||||
content: {
|
||||
|
@@ -58,7 +58,6 @@ const ChatTest = (
|
||||
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
|
||||
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
|
||||
const history = messages.slice(-historyMaxLen - 2, -2);
|
||||
console.log(history, 'history====');
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText } = await streamFetch({
|
||||
@@ -87,8 +86,6 @@ const ChatTest = (
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetChatTest() {
|
||||
console.log(ChatBoxRef.current, '===');
|
||||
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}
|
||||
@@ -147,6 +144,7 @@ const ChatTest = (
|
||||
variableModules={variableModules}
|
||||
welcomeText={welcomeText}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
@@ -12,7 +12,7 @@ const ShareModelList = ({
|
||||
onclickCollection
|
||||
}: {
|
||||
models: ShareAppItem[];
|
||||
onclickCollection: (modelId: string) => void;
|
||||
onclickCollection: (appId: string) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
|
@@ -12,8 +12,6 @@ const modelList = () => {
|
||||
const { Loading } = useLoading();
|
||||
const lastSearch = useRef('');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { refreshModel } = useUserStore();
|
||||
|
||||
/* 加载模型 */
|
||||
const {
|
||||
data: models,
|
||||
@@ -30,16 +28,15 @@ const modelList = () => {
|
||||
});
|
||||
|
||||
const onclickCollection = useCallback(
|
||||
async (modelId: string) => {
|
||||
async (appId: string) => {
|
||||
try {
|
||||
await triggerModelCollection(modelId);
|
||||
await triggerModelCollection(appId);
|
||||
getData(pageNum);
|
||||
refreshModel.removeModelDetail(modelId);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
[getData, pageNum, refreshModel]
|
||||
[getData, pageNum]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -10,13 +9,16 @@ import {
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
type HistoryItemType = {
|
||||
id: string;
|
||||
title: string;
|
||||
top?: boolean;
|
||||
};
|
||||
|
||||
const ChatHistorySlider = ({
|
||||
appName,
|
||||
appAvatar,
|
||||
@@ -24,23 +26,26 @@ const ChatHistorySlider = ({
|
||||
activeHistoryId,
|
||||
onChangeChat,
|
||||
onDelHistory,
|
||||
onSetHistoryTop,
|
||||
onCloseSlider
|
||||
}: {
|
||||
appName: string;
|
||||
appAvatar: string;
|
||||
history: {
|
||||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
history: HistoryItemType[];
|
||||
activeHistoryId: string;
|
||||
onChangeChat: (historyId?: string) => void;
|
||||
onDelHistory: (historyId: string) => void;
|
||||
onSetHistoryTop?: (e: { historyId: string; top: boolean }) => void;
|
||||
onCloseSlider: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const concatHistory = useMemo<HistoryItemType[]>(
|
||||
() => (!activeHistoryId ? [{ id: activeHistoryId, title: '新对话' }].concat(history) : history),
|
||||
[activeHistoryId, history]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
@@ -48,11 +53,11 @@ const ChatHistorySlider = ({
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
bg={'white'}
|
||||
px={[2, 5]}
|
||||
borderRight={['', theme.borders.base]}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{isPc && (
|
||||
<Flex pt={5} pb={2} alignItems={'center'} whiteSpace={'nowrap'}>
|
||||
<Flex pt={5} pb={2} px={[2, 5]} alignItems={'center'}>
|
||||
<Avatar src={appAvatar} />
|
||||
<Box ml={2} fontWeight={'bold'} className={'textEllipsis'}>
|
||||
{appName}
|
||||
@@ -60,7 +65,7 @@ const ChatHistorySlider = ({
|
||||
</Flex>
|
||||
)}
|
||||
{/* 新对话 */}
|
||||
<Box w={'100%'} h={'36px'} my={5}>
|
||||
<Box w={'100%'} px={[2, 5]} h={'36px'} my={5}>
|
||||
<Button
|
||||
variant={'base'}
|
||||
w={'100%'}
|
||||
@@ -76,8 +81,8 @@ const ChatHistorySlider = ({
|
||||
</Box>
|
||||
|
||||
{/* chat history */}
|
||||
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
|
||||
{history.map((item) => (
|
||||
<Box flex={'1 0 0'} h={0} px={[2, 5]} overflow={'overlay'}>
|
||||
{concatHistory.map((item) => (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
key={item.id}
|
||||
@@ -94,6 +99,7 @@ const ChatHistorySlider = ({
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
bg={item.top ? '#E6F6F6 !important' : ''}
|
||||
{...(item.id === activeHistoryId
|
||||
? {
|
||||
backgroundColor: 'myBlue.100 !important',
|
||||
@@ -109,49 +115,50 @@ const ChatHistorySlider = ({
|
||||
<Box flex={'1 0 0'} ml={3} className="textEllipsis">
|
||||
{item.title}
|
||||
</Box>
|
||||
<Box className="more" display={['block', 'none']}>
|
||||
<Menu autoSelect={false} isLazy offset={[0, 5]}>
|
||||
<MenuButton
|
||||
_hover={{ bg: 'white' }}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'more'} w={'14px'} p={1} />
|
||||
</MenuButton>
|
||||
<MenuList color={'myGray.700'} minW={`90px !important`}>
|
||||
<MenuItem>
|
||||
<MyIcon mr={2} name={'setTop'} w={'16px'}></MyIcon>
|
||||
置顶
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
_hover={{ color: 'red.500' }}
|
||||
{!!item.id && (
|
||||
<Box className="more" display={['block', 'none']}>
|
||||
<Menu autoSelect={false} isLazy offset={[0, 5]}>
|
||||
<MenuButton
|
||||
_hover={{ bg: 'white' }}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelHistory(item.id);
|
||||
if (item.id === activeHistoryId) {
|
||||
onChangeChat();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon mr={2} name={'delete'} w={'16px'}></MyIcon>
|
||||
删除
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
<MyIcon name={'more'} w={'14px'} p={1} />
|
||||
</MenuButton>
|
||||
<MenuList color={'myGray.700'} minW={`90px !important`}>
|
||||
{onSetHistoryTop && (
|
||||
<MenuItem
|
||||
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>
|
||||
))}
|
||||
{history.length === 0 && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
还没有聊天记录
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
|
@@ -1,304 +0,0 @@
|
||||
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;
|
@@ -1,49 +0,0 @@
|
||||
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;
|
@@ -1,212 +0,0 @@
|
||||
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;
|
77
client/src/pages/chat/components/SliderApps.tsx
Normal file
77
client/src/pages/chat/components/SliderApps.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
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;
|
File diff suppressed because it is too large
Load Diff
@@ -18,13 +18,13 @@ import { streamFetch } from '@/api/fetch';
|
||||
import { useShareChatStore, defaultHistory } from '@/store/shareChat';
|
||||
import SideBar from '@/components/SideBar';
|
||||
import { gptMessage2ChatType } from '@/utils/adapt';
|
||||
import ChatHistorySlider from './components/ChatHistorySlider';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Tag from '@/components/Tag';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import ChatHistorySlider from './components/ChatHistorySlider';
|
||||
|
||||
const ShareChat = () => {
|
||||
const theme = useTheme();
|
||||
@@ -134,6 +134,10 @@ const ShareChat = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (history.chats.length > 0) {
|
||||
ChatBoxRef.current?.scrollToBottom('auto');
|
||||
}
|
||||
|
||||
return history;
|
||||
},
|
||||
[
|
||||
@@ -152,7 +156,7 @@ const ShareChat = () => {
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']} backgroundColor={'#fdfdfd'}>
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']}>
|
||||
{/* slider */}
|
||||
{isPc ? (
|
||||
<SideBar>
|
||||
@@ -243,14 +247,7 @@ const ShareChat = () => {
|
||||
)}
|
||||
</Flex>
|
||||
{/* chat box */}
|
||||
<Box
|
||||
pt={[0, 5]}
|
||||
flex={1}
|
||||
maxW={['100%', '1000px', '1200px']}
|
||||
px={[0, 5]}
|
||||
w={'100%'}
|
||||
mx={'auto'}
|
||||
>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appAvatar={shareChatData.app.avatar}
|
||||
|
@@ -18,17 +18,16 @@ const Login = () => {
|
||||
const { isPc } = useGlobalStore();
|
||||
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
|
||||
const { setUserInfo, setLastModelId, loadKbList, setLastKbId } = useUserStore();
|
||||
const { setLastChatId, setLastChatModelId, loadHistory } = useChatStore();
|
||||
const { setLastChatId, setLastChatAppId } = useChatStore();
|
||||
|
||||
const loginSuccess = useCallback(
|
||||
(res: ResLogin) => {
|
||||
// init store
|
||||
setLastChatId('');
|
||||
setLastModelId('');
|
||||
setLastChatModelId('');
|
||||
setLastChatAppId('');
|
||||
setLastKbId('');
|
||||
loadKbList(true);
|
||||
loadHistory({ pageNum: 1, init: true });
|
||||
|
||||
setUserInfo(res.user);
|
||||
setTimeout(() => {
|
||||
@@ -37,11 +36,10 @@ const Login = () => {
|
||||
},
|
||||
[
|
||||
lastRoute,
|
||||
loadHistory,
|
||||
loadKbList,
|
||||
router,
|
||||
setLastChatId,
|
||||
setLastChatModelId,
|
||||
setLastChatAppId,
|
||||
setLastKbId,
|
||||
setLastModelId,
|
||||
setUserInfo
|
||||
|
@@ -3,6 +3,7 @@ import { getErrText } from '@/utils/tools';
|
||||
import { parseStreamChunk } from '@/utils/adapt';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { sseResponse } from '../utils/tools';
|
||||
import { SpecificInputEnum } from '@/constants/app';
|
||||
|
||||
interface Props {
|
||||
res: NextApiResponse; // 用于流转发
|
||||
@@ -13,7 +14,7 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
||||
new Promise<Record<string, any>>(async (resolve, reject) => {
|
||||
try {
|
||||
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 response = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
@@ -41,7 +42,9 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
let chatResponse: Record<string, any> = {};
|
||||
let chatResponse: Record<string, any> = {
|
||||
[SpecificInputEnum.answerText]: ''
|
||||
};
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
@@ -80,7 +83,8 @@ export const moduleFetch = ({ url, data, res }: Props) =>
|
||||
if (answer) {
|
||||
chatResponse = {
|
||||
...chatResponse,
|
||||
answer: chatResponse.answer ? chatResponse.answer + answer : answer
|
||||
[SpecificInputEnum.answerText]:
|
||||
chatResponse[SpecificInputEnum.answerText] + answer
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -8,21 +8,11 @@ const ChatSchema = new Schema({
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
modelId: {
|
||||
appId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'model',
|
||||
required: true
|
||||
},
|
||||
expiredTime: {
|
||||
// 过期时间
|
||||
type: Number,
|
||||
default: () => new Date()
|
||||
},
|
||||
loadAmount: {
|
||||
// 剩余加载次数
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
updateTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
@@ -35,13 +25,13 @@ const ChatSchema = new Schema({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
latestChat: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
top: {
|
||||
type: Boolean
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
content: {
|
||||
type: [
|
||||
{
|
||||
|
@@ -7,7 +7,7 @@ const CollectionSchema = new Schema({
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
modelId: {
|
||||
appId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'model',
|
||||
required: true
|
||||
|
@@ -1,18 +0,0 @@
|
||||
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 })
|
||||
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 }) => {
|
||||
// get shareChat
|
||||
const shareChat = await ShareChat.findOne({ shareId });
|
||||
|
@@ -1,43 +1,35 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
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 { HUMAN_ICON } from '@/constants/chat';
|
||||
|
||||
type SetShareChatHistoryItem = {
|
||||
historyId: string;
|
||||
shareId: string;
|
||||
title: string;
|
||||
latestChat: string;
|
||||
chats: ChatSiteItemType[];
|
||||
};
|
||||
|
||||
type State = {
|
||||
history: HistoryItemType[];
|
||||
loadHistory: (data: { pageNum: number; init?: boolean }) => Promise<null>;
|
||||
forbidLoadChatData: boolean;
|
||||
setForbidLoadChatData: (val: boolean) => void;
|
||||
chatData: ChatType;
|
||||
setChatData: (e?: ChatType | ((e: ChatType) => ChatType)) => void;
|
||||
lastChatModelId: string;
|
||||
setLastChatModelId: (id: string) => void;
|
||||
history: ChatHistoryItemType[];
|
||||
loadHistory: (data: { appId?: string }) => Promise<null>;
|
||||
updateHistory: (history: ChatHistoryItemType) => void;
|
||||
chatData: InitChatResponse;
|
||||
setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void;
|
||||
lastChatAppId: string;
|
||||
setLastChatAppId: (id: string) => void;
|
||||
lastChatId: string;
|
||||
setLastChatId: (id: string) => void;
|
||||
};
|
||||
|
||||
const defaultChatData: ChatType = {
|
||||
chatId: 'chatId',
|
||||
modelId: 'modelId',
|
||||
model: {
|
||||
const defaultChatData: InitChatResponse = {
|
||||
historyId: '',
|
||||
appId: '',
|
||||
app: {
|
||||
name: '',
|
||||
avatar: '/icon/logo.png',
|
||||
intro: '',
|
||||
canUse: false
|
||||
},
|
||||
chatModel: OpenAiChatEnum.GPT3516k,
|
||||
title: '新对话',
|
||||
variables: {},
|
||||
history: []
|
||||
};
|
||||
|
||||
@@ -45,10 +37,10 @@ export const useChatStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
lastChatModelId: '',
|
||||
setLastChatModelId(id: string) {
|
||||
lastChatAppId: '',
|
||||
setLastChatAppId(id: string) {
|
||||
set((state) => {
|
||||
state.lastChatModelId = id;
|
||||
state.lastChatAppId = id;
|
||||
});
|
||||
},
|
||||
lastChatId: '',
|
||||
@@ -58,10 +50,12 @@ export const useChatStore = create<State>()(
|
||||
});
|
||||
},
|
||||
history: [],
|
||||
async loadHistory({ pageNum, init = false }: { pageNum: number; init?: boolean }) {
|
||||
if (get().history.length > 0 && !init) return null;
|
||||
async loadHistory({ appId }) {
|
||||
const oneHistory = get().history[0];
|
||||
if (oneHistory && oneHistory.appId === appId) return null;
|
||||
const data = await getChatHistory({
|
||||
pageNum,
|
||||
appId,
|
||||
pageNum: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
set((state) => {
|
||||
@@ -69,14 +63,23 @@ export const useChatStore = create<State>()(
|
||||
});
|
||||
return null;
|
||||
},
|
||||
forbidLoadChatData: false,
|
||||
setForbidLoadChatData(val: boolean) {
|
||||
updateHistory(history) {
|
||||
const index = get().history.findIndex((item) => item._id === history._id);
|
||||
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,
|
||||
setChatData(e: ChatType | ((e: ChatType) => ChatType) = defaultChatData) {
|
||||
setChatData(e = defaultChatData) {
|
||||
if (typeof e === 'function') {
|
||||
set((state) => {
|
||||
state.chatData = e(state.chatData);
|
||||
@@ -91,7 +94,7 @@ export const useChatStore = create<State>()(
|
||||
{
|
||||
name: 'chatStore',
|
||||
partialize: (state) => ({
|
||||
lastChatModelId: state.lastChatModelId,
|
||||
lastChatAppId: state.lastChatAppId,
|
||||
lastChatId: state.lastChatId
|
||||
})
|
||||
}
|
||||
|
@@ -74,8 +74,7 @@ export const useUserStore = create<State>()(
|
||||
async loadMyModels() {
|
||||
const res = await getMyModels();
|
||||
set((state) => {
|
||||
state.myApps = res.myApps;
|
||||
state.myCollectionApps = res.myCollectionApps;
|
||||
state.myApps = res;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
|
4
client/src/types/chat.d.ts
vendored
4
client/src/types/chat.d.ts
vendored
@@ -18,10 +18,6 @@ export type ChatSiteItemType = {
|
||||
status: 'loading' | 'finish';
|
||||
} & ChatItemType;
|
||||
|
||||
export interface ChatType extends InitChatResponse {
|
||||
history: ChatSiteItemType[];
|
||||
}
|
||||
|
||||
export type HistoryItemType = {
|
||||
_id: string;
|
||||
updateTime: Date;
|
||||
|
11
client/src/types/mongoSchema.d.ts
vendored
11
client/src/types/mongoSchema.d.ts
vendored
@@ -58,7 +58,7 @@ export interface AppSchema {
|
||||
}
|
||||
|
||||
export interface CollectionSchema {
|
||||
modelId: string;
|
||||
appId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
@@ -78,19 +78,14 @@ export interface TrainingDataSchema {
|
||||
export interface ChatSchema {
|
||||
_id: string;
|
||||
userId: string;
|
||||
modelId: string;
|
||||
expiredTime: number;
|
||||
appId: string;
|
||||
updateTime: Date;
|
||||
title: string;
|
||||
customTitle: string;
|
||||
latestChat: string;
|
||||
top: boolean;
|
||||
variables: Record<string, any>;
|
||||
content: ChatItemType[];
|
||||
}
|
||||
export interface ChatPopulate extends ChatSchema {
|
||||
userId: UserModelSchema;
|
||||
modelId: AppSchema;
|
||||
}
|
||||
|
||||
export interface BillSchema {
|
||||
_id: string;
|
||||
|
Reference in New Issue
Block a user