Commit d31bdf0e by archer

feat: share chat page

parent d3e79230
import { GET, POST, DELETE } from './request'; import { GET, POST, DELETE } from './request';
import type { ChatItemType, HistoryItemType } from '@/types/chat'; import type { ChatItemType, HistoryItemType } from '@/types/chat';
import type { InitChatResponse } from './response/chat'; import type { InitChatResponse, InitShareChatResponse } from './response/chat';
import { RequestPaging } from '../types/index'; import { RequestPaging } from '../types/index';
import type { ShareChatSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/model';
import { Obj2Query } from '@/utils/tools';
/** /**
* 获取初始化聊天内容 * 获取初始化聊天内容
...@@ -35,3 +38,29 @@ export const postSaveChat = (data: { ...@@ -35,3 +38,29 @@ export const postSaveChat = (data: {
*/ */
export const delChatRecordByIndex = (chatId: string, contentId: string) => export const delChatRecordByIndex = (chatId: string, contentId: string) =>
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`); DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`);
/**
* create a shareChat
*/
export const createShareChat = (
data: ShareChatEditType & {
modelId: string;
}
) => POST<string>(`/chat/shareChat/create`, data);
/**
* get shareChat
*/
export const getShareChatList = (modelId: string) =>
GET<ShareChatSchema[]>(`/chat/shareChat/list?modelId=${modelId}`);
/**
* delete a shareChat
*/
export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?id=${id}`);
/**
* 初始化分享聊天
*/
export const initShareChatInfo = (data: { shareId: string; password: string }) =>
GET<InitShareChatResponse>(`/chat/shareChat/init?${Obj2Query(data)}`);
...@@ -13,3 +13,13 @@ export interface InitChatResponse { ...@@ -13,3 +13,13 @@ export interface InitChatResponse {
chatModel: ModelSchema['chat']['chatModel']; // 对话模型名 chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
history: ChatItemType[]; history: ChatItemType[];
} }
export interface InitShareChatResponse {
maxContext: number;
model: {
name: string;
avatar: string;
intro: string;
};
chatModel: ModelSchema['chat']['chatModel']; // 对话模型名
}
...@@ -10,11 +10,13 @@ import NavbarPhone from './navbarPhone'; ...@@ -10,11 +10,13 @@ import NavbarPhone from './navbarPhone';
const pcUnShowLayoutRoute: Record<string, boolean> = { const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true, '/': true,
'/login': true '/login': true,
'/chat/share': true
}; };
const phoneUnShowLayoutRoute: Record<string, boolean> = { const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/': true, '/': true,
'/login': true '/login': true,
'/chat/share': true
}; };
const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: boolean }) => { const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: boolean }) => {
...@@ -67,7 +69,7 @@ const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: b ...@@ -67,7 +69,7 @@ const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: b
</Flex> </Flex>
)} )}
</Box> </Box>
{loading && <Loading />} <Loading loading={loading} />
</> </>
); );
}; };
......
...@@ -5,7 +5,7 @@ const Loading = ({ fixed = true }: { fixed?: boolean }) => { ...@@ -5,7 +5,7 @@ const Loading = ({ fixed = true }: { fixed?: boolean }) => {
return ( return (
<Flex <Flex
position={fixed ? 'fixed' : 'absolute'} position={fixed ? 'fixed' : 'absolute'}
zIndex={100} zIndex={10000}
backgroundColor={'rgba(255,255,255,0.5)'} backgroundColor={'rgba(255,255,255,0.5)'}
top={0} top={0}
left={0} left={0}
......
import { getSystemModelList } from '@/api/system'; import { getSystemModelList } from '@/api/system';
import type { ModelSchema } from '@/types/mongoSchema'; import type { ModelSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/model';
export const embeddingModel = 'text-embedding-ada-002'; export const embeddingModel = 'text-embedding-ada-002';
export type EmbeddingModelType = 'text-embedding-ada-002'; export type EmbeddingModelType = 'text-embedding-ada-002';
...@@ -161,3 +162,9 @@ export const defaultModel: ModelSchema = { ...@@ -161,3 +162,9 @@ export const defaultModel: ModelSchema = {
maxLoadAmount: 1 maxLoadAmount: 1
} }
}; };
export const defaultShareChat: ShareChatEditType = {
name: '',
password: '',
maxContext: 5
};
function Error({ statusCode }: { statusCode: number }) { function Error({ errStr }: { errStr: string }) {
return ( return <p>{errStr}</p>;
<p>
{statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'}
</p>
);
} }
Error.getInitialProps = ({ res, err }: { res: any; err: any }) => { Error.getInitialProps = ({ res, err }: { res: any; err: any }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
console.log(err); console.log(err);
return { statusCode }; return { errStr: JSON.stringify(err) };
}; };
export default Error; export default Error;
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { authShareChat } from '@/service/utils/auth';
import { modelServiceToolMap } from '@/service/utils/chat';
import { ChatItemSimpleType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import { PassThrough } from 'stream';
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat';
import { searchKb } from '@/service/plugins/searchKb';
import { ChatRoleEnum } from '@/constants/chat';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let step = 0; // step=1 时,表示开始了流响应
const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
try {
const { shareId, password, historyId, prompts } = req.body as {
prompts: ChatItemSimpleType[];
password: string;
shareId: string;
historyId: string;
};
if (!historyId || !prompts) {
throw new Error('分享链接无效');
}
await connectToDatabase();
let startTime = Date.now();
const { model, showModelDetail, userOpenAiKey, systemAuthKey, userId } = await authShareChat({
shareId,
password
});
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// 使用了知识库搜索
if (model.chat.useKb) {
const { code, searchPrompts } = await searchKb({
userOpenAiKey,
prompts,
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
model,
userId
});
// search result is empty
if (code === 201) {
return res.send(searchPrompts[0]?.value);
}
prompts.splice(prompts.length - 3, 0, ...searchPrompts);
} else {
// 没有用知识库搜索,仅用系统提示词
model.chat.systemPrompt &&
prompts.splice(prompts.length - 3, 0, {
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
});
}
// 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2
);
// 发出请求
const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature,
messages: prompts,
stream: true,
res,
chatId: historyId
});
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
const { totalTokens, finishMessages } = await resStreamResponse({
model: model.chat.chatModel,
res,
stream,
chatResponse: streamResponse,
prompts,
systemPrompt: ''
});
/* bill */
pushChatBill({
isPay: !userOpenAiKey,
chatModel: model.chat.chatModel,
userId,
textLen: finishMessages.map((item) => item.value).join('').length,
tokens: totalTokens
});
updateShareChatBill({
shareId,
tokens: totalTokens
});
} catch (err: any) {
if (step === 1) {
// 直接结束流
console.log('error,结束');
stream.destroy();
} else {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ShareChat } from '@/service/mongo';
import { authModel, authToken } from '@/service/utils/auth';
import type { ShareChatEditType } from '@/types/model';
/* create a shareChat */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { modelId, name, maxContext, password } = req.body as ShareChatEditType & {
modelId: string;
};
await connectToDatabase();
const userId = await authToken(req);
await authModel({
modelId,
userId
});
const { _id } = await ShareChat.create({
userId,
modelId,
name,
password,
maxContext
});
jsonRes(res, {
data: _id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ShareChat } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
/* delete a shareChat by shareChatId */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { id } = req.query as {
id: string;
};
await connectToDatabase();
const userId = await authToken(req);
await ShareChat.findOneAndRemove({
_id: id,
userId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ShareChat } from '@/service/mongo';
import type { InitShareChatResponse } from '@/api/response/chat';
import { authModel } from '@/service/utils/auth';
import { hashPassword } from '@/service/utils/tools';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { shareId, password = '' } = req.query as {
shareId: string;
password: string;
};
if (!shareId) {
throw new Error('params is error');
}
await connectToDatabase();
// get shareChat
const shareChat = await ShareChat.findById(shareId);
if (!shareChat) {
throw new Error('分享链接已失效');
}
if (shareChat.password !== hashPassword(password)) {
return jsonRes(res, {
code: 501,
message: '密码不正确'
});
}
// 校验使用权限
const { model } = await authModel({
modelId: shareChat.modelId,
userId: String(shareChat.userId)
});
jsonRes<InitShareChatResponse>(res, {
data: {
maxContext: shareChat.maxContext,
model: {
name: model.name,
avatar: model.avatar,
intro: model.share.intro
},
chatModel: model.chat.chatModel
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ShareChat } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { hashPassword } from '@/service/utils/tools';
/* get shareChat list by modelId */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { modelId } = req.query as {
modelId: string;
};
await connectToDatabase();
const userId = await authToken(req);
const data = await ShareChat.find({
modelId,
userId
}).sort({
_id: -1
});
const blankPassword = hashPassword('');
jsonRes(res, {
data: data.map((item) => ({
_id: item._id,
name: item.name,
password: item.password === blankPassword ? '' : '1',
tokens: item.tokens,
maxContext: item.maxContext,
lastTime: item.lastTime
}))
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
...@@ -26,21 +26,23 @@ const Empty = ({ ...@@ -26,21 +26,23 @@ const Empty = ({
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
> >
<Card p={4} mb={10}> {name && (
<Flex mb={2} alignItems={'center'} justifyContent={'center'}> <Card p={4} mb={10}>
<Image <Flex mb={2} alignItems={'center'} justifyContent={'center'}>
src={avatar || LOGO_ICON} <Image
w={'32px'} src={avatar || LOGO_ICON}
maxH={'40px'} w={'32px'}
objectFit={'contain'} maxH={'40px'}
alt={''} objectFit={'contain'}
/> alt={''}
<Box ml={3} fontSize={'3xl'} fontWeight={'bold'}> />
{name} <Box ml={3} fontSize={'3xl'} fontWeight={'bold'}>
</Box> {name}
</Flex> </Box>
<Box whiteSpace={'pre-line'}>{intro}</Box> </Flex>
</Card> <Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}
{/* version intro */} {/* version intro */}
<Card p={4} mb={10}> <Card p={4} mb={10}>
<Markdown source={versionIntro} /> <Markdown source={versionIntro} />
......
import React, { useCallback, useRef, useState } from 'react';
import type { MouseEvent } from 'react';
import { AddIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Flex,
useTheme,
Menu,
MenuList,
MenuItem,
useOutsideClick
} from '@chakra-ui/react';
import { ChatIcon } from '@chakra-ui/icons';
import { useRouter } from 'next/router';
import { formatTimeToChatTime } from '@/utils/tools';
import MyIcon from '@/components/Icon';
import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import { useScreen } from '@/hooks/useScreen';
import styles from '../index.module.scss';
const PcSliderBar = ({
isPcDevice,
onclickDelHistory,
onclickExportChat,
onCloseSlider
}: {
isPcDevice: boolean;
onclickDelHistory: (historyId: string) => void;
onclickExportChat: (type: ExportChatType) => void;
onCloseSlider: () => void;
}) => {
const router = useRouter();
const { shareId = '', historyId = '' } = router.query as { shareId: string; historyId: string };
const theme = useTheme();
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const ContextMenuRef = useRef(null);
const [contextMenuData, setContextMenuData] = useState<{
left: number;
top: number;
history: ShareChatHistoryItemType;
}>();
const { shareChatHistory } = useChatStore();
// close contextMenu
useOutsideClick({
ref: ContextMenuRef,
handler: () =>
setTimeout(() => {
setContextMenuData(undefined);
})
});
const onclickContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>, history: ShareChatHistoryItemType) => {
e.preventDefault(); // 阻止默认右键菜单
if (!isPc) return;
setContextMenuData({
left: e.clientX + 15,
top: e.clientY + 10,
history
});
},
[isPc]
);
const replaceChatPage = useCallback(
({ hId = '', shareId }: { hId?: string; shareId: string }) => {
if (hId === historyId) return;
router.replace(`/chat/share?shareId=${shareId}&historyId=${hId}`);
!isPc && onCloseSlider();
},
[historyId, isPc, onCloseSlider, router]
);
return (
<Flex
position={'relative'}
flexDirection={'column'}
w={'100%'}
h={'100%'}
bg={'white'}
borderRight={['', theme.borders.base]}
>
{/* 新对话 */}
<Box
className={styles.newChat}
zIndex={1000}
w={'90%'}
h={'40px'}
my={5}
mx={'auto'}
position={'relative'}
>
<Button
variant={'base'}
w={'100%'}
h={'100%'}
leftIcon={<AddIcon />}
onClick={() => replaceChatPage({ shareId })}
>
新对话
</Button>
</Box>
{/* chat history */}
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
{shareChatHistory.map((item) => (
<Flex
position={'relative'}
key={item._id}
alignItems={'center'}
py={3}
pr={[0, 3]}
pl={[6, 3]}
cursor={'pointer'}
transition={'background-color .2s ease-in'}
borderLeft={['none', '5px solid transparent']}
userSelect={'none'}
_hover={{
backgroundColor: ['', '#dee0e3']
}}
{...(item._id === historyId
? {
backgroundColor: '#eff0f1',
borderLeftColor: 'myBlue.600 !important'
}
: {})}
onClick={() => replaceChatPage({ hId: item._id, shareId: item.shareId })}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<ChatIcon fontSize={'16px'} color={'myGray.500'} />
<Box flex={'1 0 0'} w={0} ml={3}>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} w={0} className="textEllipsis" color={'myGray.1000'}>
{item.title}
</Box>
<Box color={'myGray.400'} fontSize={'sm'}>
{formatTimeToChatTime(item.updateTime)}
</Box>
</Flex>
<Box className="textEllipsis" mt={1} fontSize={'sm'} color={'myGray.500'}>
{item.latestChat || '……'}
</Box>
</Box>
{/* phone quick delete */}
{!isPc && (
<MyIcon
px={3}
name={'delete'}
w={'16px'}
onClickCapture={(e) => {
e.stopPropagation();
onclickDelHistory(item._id);
item._id === historyId && replaceChatPage({ shareId: item.shareId });
}}
/>
)}
</Flex>
))}
{shareChatHistory.length === 0 && (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
还没有聊天记录
</Box>
</Flex>
)}
</Box>
{/* context menu */}
{contextMenuData && (
<Box zIndex={10} position={'fixed'} top={contextMenuData.top} left={contextMenuData.left}>
<Box ref={ContextMenuRef}></Box>
<Menu isOpen>
<MenuList>
<MenuItem
onClick={() => {
onclickDelHistory(contextMenuData.history._id);
contextMenuData.history._id === historyId && replaceChatPage({ shareId });
}}
>
删除记录
</MenuItem>
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('md')}>导出Markdown格式</MenuItem>
</MenuList>
</Menu>
</Box>
)}
</Flex>
);
};
export default PcSliderBar;
import { connectToDatabase, Bill, User } from '../mongo'; import { connectToDatabase, Bill, User, ShareChat } from '../mongo';
import { ChatModelMap, OpenAiChatEnum, ChatModelType, embeddingModel } from '@/constants/model'; import { ChatModelMap, OpenAiChatEnum, ChatModelType, embeddingModel } from '@/constants/model';
import { BillTypeEnum } from '@/constants/user'; import { BillTypeEnum } from '@/constants/user';
...@@ -55,6 +55,23 @@ export const pushChatBill = async ({ ...@@ -55,6 +55,23 @@ export const pushChatBill = async ({
} }
}; };
export const updateShareChatBill = async ({
shareId,
tokens
}: {
shareId: string;
tokens: number;
}) => {
try {
await ShareChat.findByIdAndUpdate(shareId, {
$inc: { tokens },
lastTime: new Date()
});
} catch (error) {
console.log('update shareChat error', error);
}
};
export const pushSplitDataBill = async ({ export const pushSplitDataBill = async ({
isPay, isPay,
userId, userId,
......
import { Schema, model, models, Model } from 'mongoose';
import { ShareChatSchema as ShareChatSchemaType } from '@/types/mongoSchema';
import { hashPassword } from '@/service/utils/tools';
const ShareChatSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
modelId: {
type: Schema.Types.ObjectId,
ref: 'model',
required: true
},
name: {
type: String,
required: true
},
password: {
type: String,
set: (val: string) => hashPassword(val)
},
tokens: {
type: Number,
default: 0
},
maxContext: {
type: Number,
default: 20
},
lastTime: {
type: Date
}
});
export const ShareChat: Model<ShareChatSchemaType> =
models['shareChat'] || model('shareChat', ShareChatSchema);
...@@ -51,3 +51,4 @@ export * from './models/splitData'; ...@@ -51,3 +51,4 @@ export * from './models/splitData';
export * from './models/openapi'; export * from './models/openapi';
export * from './models/promotionRecord'; export * from './models/promotionRecord';
export * from './models/collection'; export * from './models/collection';
export * from './models/shareChat';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import cookie from 'cookie'; import cookie from 'cookie';
import { Chat, Model, OpenApi, User } from '../mongo'; import { Chat, Model, OpenApi, User, ShareChat } from '../mongo';
import type { ModelSchema } from '@/types/mongoSchema'; import type { ModelSchema } from '@/types/mongoSchema';
import type { ChatItemSimpleType } from '@/types/chat'; import type { ChatItemSimpleType } from '@/types/chat';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
...@@ -9,6 +9,7 @@ import { ClaudeEnum, defaultModel } from '@/constants/model'; ...@@ -9,6 +9,7 @@ import { ClaudeEnum, defaultModel } from '@/constants/model';
import { formatPrice } from '@/utils/user'; import { formatPrice } from '@/utils/user';
import { ERROR_ENUM } from '../errorCode'; import { ERROR_ENUM } from '../errorCode';
import { ChatModelType, OpenAiChatEnum } from '@/constants/model'; import { ChatModelType, OpenAiChatEnum } from '@/constants/model';
import { hashPassword } from '@/service/utils/tools';
/* 校验 token */ /* 校验 token */
export const authToken = (req: NextApiRequest): Promise<string> => { export const authToken = (req: NextApiRequest): Promise<string> => {
...@@ -113,8 +114,8 @@ export const authModel = async ({ ...@@ -113,8 +114,8 @@ export const authModel = async ({
1. authOwner=true or authUser = true , just owner can use 1. authOwner=true or authUser = true , just owner can use
2. authUser = false and share, anyone can use 2. authUser = false and share, anyone can use
*/ */
if ((authOwner || (authUser && !model.share.isShare)) && userId !== String(model.userId)) { if (authOwner || (authUser && !model.share.isShare)) {
return Promise.reject(ERROR_ENUM.unAuthModel); if (userId !== String(model.userId)) return Promise.reject(ERROR_ENUM.unAuthModel);
} }
// do not share detail info // do not share detail info
...@@ -183,6 +184,50 @@ export const authChat = async ({ ...@@ -183,6 +184,50 @@ export const authChat = async ({
showModelDetail showModelDetail
}; };
}; };
export const authShareChat = async ({
shareId,
password
}: {
shareId: string;
password: string;
}) => {
// get shareChat
const shareChat = await ShareChat.findById(shareId);
if (!shareChat) {
return Promise.reject('分享链接已失效');
}
if (shareChat.password !== hashPassword(password)) {
return Promise.reject({
code: 501,
message: '密码不正确'
});
}
const modelId = String(shareChat.modelId);
const userId = String(shareChat.userId);
// 获取 model 数据
const { model, showModelDetail } = await authModel({
modelId,
userId
});
// 获取 user 的 apiKey
const { userOpenAiKey, systemAuthKey } = await getApiKey({
model: model.chat.chatModel,
userId
});
return {
userOpenAiKey,
systemAuthKey,
userId,
model,
showModelDetail
};
};
/* 校验 open api key */ /* 校验 open api key */
export const authOpenApiKey = async (req: NextApiRequest) => { export const authOpenApiKey = async (req: NextApiRequest) => {
......
...@@ -3,9 +3,23 @@ import { devtools, persist } from 'zustand/middleware'; ...@@ -3,9 +3,23 @@ import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { OpenAiChatEnum } from '@/constants/model'; import { OpenAiChatEnum } from '@/constants/model';
import { HistoryItemType, ChatType } from '@/types/chat'; import {
ChatSiteItemType,
HistoryItemType,
ShareChatHistoryItemType,
ChatType,
ShareChatType
} from '@/types/chat';
import { getChatHistory } from '@/api/chat'; import { getChatHistory } from '@/api/chat';
type SetShareChatHistoryItem = {
historyId: string;
shareId: string;
title: string;
latestChat: string;
chats: ChatSiteItemType[];
};
type State = { type State = {
history: HistoryItemType[]; history: HistoryItemType[];
loadHistory: (data: { pageNum: number; init?: boolean }) => Promise<null>; loadHistory: (data: { pageNum: number; init?: boolean }) => Promise<null>;
...@@ -17,6 +31,16 @@ type State = { ...@@ -17,6 +31,16 @@ type State = {
setLastChatModelId: (id: string) => void; setLastChatModelId: (id: string) => void;
lastChatId: string; lastChatId: string;
setLastChatId: (id: string) => void; setLastChatId: (id: string) => void;
shareChatData: ShareChatType;
setShareChatData: (e?: ShareChatType | ((e: ShareChatType) => ShareChatType)) => void;
password: string;
setPassword: (val: string) => void;
shareChatHistory: ShareChatHistoryItemType[];
setShareChatHistory: (e: SetShareChatHistoryItem) => void;
delShareHistoryById: (historyId: string) => void;
delShareChatHistoryItemById: (historyId: string, index: number) => void;
delShareChatHistory: (shareId?: string) => void;
}; };
const defaultChatData = { const defaultChatData = {
...@@ -31,6 +55,16 @@ const defaultChatData = { ...@@ -31,6 +55,16 @@ const defaultChatData = {
chatModel: OpenAiChatEnum.GPT35, chatModel: OpenAiChatEnum.GPT35,
history: [] history: []
}; };
const defaultShareChatData: ShareChatType = {
maxContext: 5,
model: {
name: '',
avatar: '/icon/logo.png',
intro: ''
},
chatModel: 'gpt-3.5-turbo',
history: []
};
export const useChatStore = create<State>()( export const useChatStore = create<State>()(
devtools( devtools(
...@@ -77,13 +111,114 @@ export const useChatStore = create<State>()( ...@@ -77,13 +111,114 @@ export const useChatStore = create<State>()(
state.chatData = e; state.chatData = e;
}); });
} }
},
shareChatData: defaultShareChatData,
setShareChatData(
e: ShareChatType | ((e: ShareChatType) => ShareChatType) = defaultShareChatData
) {
if (typeof e === 'function') {
set((state) => {
state.shareChatData = e(state.shareChatData);
});
} else {
set((state) => {
state.shareChatData = e;
});
}
},
password: '',
setPassword(val: string) {
set((state) => {
state.password = val;
});
},
shareChatHistory: [],
setShareChatHistory({
historyId,
shareId,
title,
latestChat,
chats = []
}: SetShareChatHistoryItem) {
set((state) => {
const history = state.shareChatHistory.find((item) => item._id === historyId);
let historyList: ShareChatHistoryItemType[] = [];
if (history) {
historyList = state.shareChatHistory.map((item) =>
item._id === historyId
? {
...item,
title,
latestChat,
updateTime: new Date(),
chats
}
: item
);
} else {
historyList = [
...state.shareChatHistory,
{
_id: historyId,
shareId,
title,
latestChat,
updateTime: new Date(),
chats
}
];
}
// @ts-ignore
historyList.sort((a, b) => new Date(b.updateTime) - new Date(a.updateTime));
state.shareChatHistory = historyList.slice(0, 30);
});
},
delShareHistoryById(historyId: string) {
set((state) => {
state.shareChatHistory = state.shareChatHistory.filter(
(item) => item._id !== historyId
);
});
},
delShareChatHistoryItemById(historyId: string, index: number) {
set((state) => {
// update history store
const newHistoryList = state.shareChatHistory.map((item) =>
item._id === historyId
? {
...item,
chats: [...item.chats.slice(0, index), ...item.chats.slice(index + 1)]
}
: item
);
state.shareChatHistory = newHistoryList;
// update chatData
state.shareChatData.history =
newHistoryList.find((item) => item._id === historyId)?.chats || [];
});
},
delShareChatHistory(shareId?: string) {
set((state) => {
if (shareId) {
state.shareChatHistory = state.shareChatHistory.filter(
(item) => item.shareId !== shareId
);
} else {
state.shareChatHistory = [];
}
});
} }
})), })),
{ {
name: 'globalStore', name: 'chatStore',
partialize: (state) => ({ partialize: (state) => ({
lastChatModelId: state.lastChatModelId, lastChatModelId: state.lastChatModelId,
lastChatId: state.lastChatId lastChatId: state.lastChatId,
password: state.password,
shareChatHistory: state.shareChatHistory
}) })
} }
) )
......
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import type { InitChatResponse } from '@/api/response/chat'; import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
export type ExportChatType = 'md' | 'pdf' | 'html'; export type ExportChatType = 'md' | 'pdf' | 'html';
...@@ -20,6 +20,10 @@ export interface ChatType extends InitChatResponse { ...@@ -20,6 +20,10 @@ export interface ChatType extends InitChatResponse {
history: ChatSiteItemType[]; history: ChatSiteItemType[];
} }
export interface ShareChatType extends InitShareChatResponse {
history: ChatSiteItemType[];
}
export type HistoryItemType = { export type HistoryItemType = {
_id: string; _id: string;
updateTime: Date; updateTime: Date;
...@@ -27,3 +31,12 @@ export type HistoryItemType = { ...@@ -27,3 +31,12 @@ export type HistoryItemType = {
title: string; title: string;
latestChat: string; latestChat: string;
}; };
export type ShareChatHistoryItemType = {
_id: string;
shareId: string;
updateTime: Date;
title: string;
latestChat: string;
chats: ChatSiteItemType[];
};
...@@ -33,3 +33,9 @@ export interface ShareModelItem { ...@@ -33,3 +33,9 @@ export interface ShareModelItem {
share: ModelSchema['share']; share: ModelSchema['share'];
isCollection: boolean; isCollection: boolean;
} }
export type ShareChatEditType = {
name: string;
password: string;
maxContext: number;
};
...@@ -137,3 +137,14 @@ export interface PromotionRecordSchema { ...@@ -137,3 +137,14 @@ export interface PromotionRecordSchema {
createTime: Date; // 记录时间 createTime: Date; // 记录时间
amount: number; amount: number;
} }
export interface ShareChatSchema {
_id: string;
userId: string;
modelId: string;
password: string;
name: string;
tokens: number;
maxContext: number;
lastTime: Date;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment