feat: 修改chat的数据结构

This commit is contained in:
Archer
2023-03-18 00:49:44 +08:00
parent e6c9ca540a
commit 38c093d9ae
33 changed files with 2631 additions and 341 deletions

View File

@@ -1,3 +1,6 @@
{ {
"extends": "next/core-web-vitals" "extends": "next/core-web-vitals",
"rules": {
"react-hooks/rules-of-hooks": 0
}
} }

View File

@@ -6,7 +6,18 @@ const isDev = process.env.NODE_ENV === 'development';
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
reactStrictMode: false, reactStrictMode: false,
compress: true compress: true,
webpack(config) {
config.module.rules = config.module.rules.concat([
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack']
}
]);
return config;
}
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@@ -47,6 +47,7 @@
"zustand": "^4.3.5" "zustand": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@svgr/webpack": "^6.5.1",
"@types/formidable": "^2.0.5", "@types/formidable": "^2.0.5",
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/node": "18.14.0", "@types/node": "18.14.0",

2196
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { GET, POST, DELETE } from './request'; import { GET, POST, DELETE } from './request';
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat'; import type { ChatItemType, ChatSiteItemType } from '@/types/chat';
import axios from 'axios'; import type { InitChatResponse } from './response/chat';
/** /**
* 获取一个聊天框的ID * 获取一个聊天框的ID
@@ -10,12 +10,8 @@ export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?mo
/** /**
* 获取初始化聊天内容 * 获取初始化聊天内容
*/ */
export const getInitChatSiteInfo = (chatId: string, windowId: string = '') => export const getInitChatSiteInfo = (chatId: string) =>
GET<{ GET<InitChatResponse>(`/chat/init?chatId=${chatId}`);
windowId: string;
chatSite: ChatSiteType;
history: ChatItemType[];
}>(`/chat/init?chatId=${chatId}&windowId=${windowId}`);
/** /**
* 发送 GPT3 prompt * 发送 GPT3 prompt
@@ -38,11 +34,10 @@ export const postGPT3SendPrompt = ({
/** /**
* 存储一轮对话 * 存储一轮对话
*/ */
export const postSaveChat = (data: { windowId: string; prompts: ChatItemType[] }) => export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] }) =>
POST('/chat/saveChat', data); POST('/chat/saveChat', data);
/** /**
* 删除最后一句 * 删除最后一句
*/ */
export const delLastMessage = (windowId?: string) => export const delLastMessage = (chatId: string) => DELETE(`/chat/delLastMessage?chatId=${chatId}`);
windowId ? DELETE(`/chat/delLastMessage?windowId=${windowId}`) : null;

13
src/api/response/chat.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import type { ChatPopulate, ModelSchema } from '@/types/mongoSchema';
import type { ChatItemType } from '@/types/chat';
export type InitChatResponse = {
chatId: string;
modelId: string;
name: string;
avatar: string;
secret: ModelSchema.secret;
chatModel: ModelSchema.service.ChatModel; // 模型名
history: ChatItemType[];
isExpiredTime: boolean;
};

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679070302676" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M910.13 243.22L545.97 32.97c-19.82-11.46-44.41-11.4-64.16 0.13L115.54 246.51c-19.5 11.36-31.68 32.43-31.76 54.99L82.1 725.44c-0.08 22.87 12.16 44.16 31.97 55.6l364.16 210.25c9.86 5.7 20.92 8.55 31.97 8.55 11.13 0 22.27-2.89 32.19-8.67l366.27-213.41c19.5-11.36 31.66-32.43 31.75-54.99l1.69-423.93c0.08-22.88-12.16-44.18-31.97-55.62zM513.68 88.9l335.28 193.58-332.93 192.2c-1.38 0.8-2.63 1.76-3.94 2.64-1.32-0.88-2.56-1.85-3.94-2.64L178.66 284.46 513.68 88.9zM146.69 725.68l1.24-384.39 327.91 189.32c1.59 0.92 2.74 2.31 3.54 3.89-0.09 1.49-0.29 2.95-0.28 4.45l0.7 175.55-0.8 202.69-332.31-191.51z m398.5 189.44l-0.8-200.61 0.7-175.54c0.01-1.5-0.2-2.97-0.28-4.46 0.8-1.59 1.95-2.98 3.53-3.9l329.03-189.96-1.23 381.29-330.95 193.18z" p-id="1174"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679070718083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5975" id="mx_n_1679070718084" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M1023.82 694.91v146.26c0 102.38-80.44 182.82-182.82 182.82H182.83C80.45 1024 0 943.56 0 841.18V183.01C0 80.63 80.45 0.19 182.83 0.19h146.26c21.94 0 36.57 14.62 36.57 36.57 0 21.94-14.62 36.56-36.57 36.56H182.83c-58.5 0-109.7 51.19-109.7 109.7v658.17c0 58.5 51.19 109.7 109.7 109.7h658.17c58.5 0 109.7-51.19 109.7-109.7V694.91c0-21.94 14.62-36.56 36.56-36.56 21.93 0 36.56 14.63 36.56 36.56z" p-id="5976"></path><path d="M1012.6 292.61L684.73 5.86c-6.56-5.7-15.02-6.32-21.96-1.49-6.94 4.83-11.31 14.24-11.31 24.65v132.66h-80.9c-84.89 0-164.74 41.49-224.82 116.92-29.27 36.79-52.28 79.65-68.44 127.34C260.57 455.5 252.11 508.02 252.11 562.27c0 40.13 4.65 85.72 12.17 118.79 2.47 11.02 9.89 18.95 18.72 19.94h1.81c8.08 0 15.59-6.07 19.21-15.61 50.29-134.27 154.86-220.98 266.46-220.98h80.9v138.12c0 10.28 4.37 19.69 11.31 24.65 6.94 4.83 15.4 4.33 21.96-1.49l327.96-286.75c5.89-5.21 9.51-13.87 9.51-23.16-0.01-9.3-3.53-17.97-9.52-23.17z m-88.21 16.04L717.4 477.58v-81.92c0-11.07-7.41-20.08-16.52-20.08h-78.98c-49.84 0-95.79 2.5-146.79 32.25-30.31 17.68-130.92 89.06-150 121.09-0.51-7.94 20.14-85.47 23-92.41C390.11 334.54 504.6 237.8 621.9 237.8h78.98c9.1 0 16.52-9.02 16.52-20.08v-78l206.99 168.93z" p-id="5977"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,23 +1,20 @@
type TIconfont = { import React from 'react';
name: string; import type { IconProps } from '@chakra-ui/react';
color?: string; import { Icon } from '@chakra-ui/react';
width?: number | string; import dynamic from 'next/dynamic';
height?: number | string;
className?: string; const map = {
model: dynamic(() => import('./icons/model.svg')),
share: dynamic(() => import('./icons/share.svg'))
}; };
function Icon({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) { const MyIcon = ({
const style = { name,
fill: color, w = 'auto',
width, h = 'auto',
height ...props
}; }: { name: keyof typeof map } & IconProps) => {
return map[name] ? <Icon as={map[name]} w={w} h={h} {...props} /> : null;
};
return ( export default MyIcon;
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
<use xlinkHref={`#${name}`}></use>
</svg>
);
}
export default Icon;

View File

@@ -0,0 +1,23 @@
type TIconfont = {
name: string;
color?: string;
width?: number | string;
height?: number | string;
className?: string;
};
function Iconfont({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) {
const style = {
fill: color,
width,
height
};
return (
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
<use xlinkHref={`#${name}`}></use>
</svg>
);
}
export default Iconfont;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Icon from '../Icon'; import Icon from '../Iconfont';
export enum NavbarTypeEnum { export enum NavbarTypeEnum {
normal = 'normal', normal = 'normal',

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Icon from '../Icon'; import Icon from '../Iconfont';
import { import {
Flex, Flex,
Drawer, Drawer,

View File

@@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools'; import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon'; import Icon from '@/components/Iconfont';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';

View File

@@ -31,13 +31,10 @@ export const introPage = `
### 对话框介绍 ### 对话框介绍
1. 每个对话框以 windowId 作为标识。 1. 每个对话框以 chatId 作为标识。
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。 2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。 3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如: 4. 如果想分享一个纯的对话框,请点击侧边栏的分享按键。例如:
* 当前网页链接http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
* 分享链接应为http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
### 其他问题 ### 其他问题
还有其他问题,可以加我 wx: YNyiqi拉个交流群大家一起聊聊。 还有其他问题,可以加我 wx: YNyiqi拉个交流群大家一起聊聊。

View File

@@ -1,18 +1,18 @@
export enum OpenAiModelEnum { export enum ChatModelNameEnum {
GPT35 = 'gpt-3.5-turbo', GPT35 = 'gpt-3.5-turbo',
GPT3 = 'text-davinci-003' GPT3 = 'text-davinci-003'
} }
export const OpenAiList = [ export const OpenAiList = [
{ {
name: 'chatGPT', name: 'chatGPT',
model: OpenAiModelEnum.GPT35, model: ChatModelNameEnum.GPT35,
trainName: 'turbo', trainName: 'turbo',
canTraining: false, canTraining: false,
maxToken: 4060 maxToken: 4060
}, },
{ {
name: 'GPT3', name: 'GPT3',
model: OpenAiModelEnum.GPT3, model: ChatModelNameEnum.GPT3,
trainName: 'davinci', trainName: 'davinci',
canTraining: true, canTraining: true,
maxToken: 4060 maxToken: 4060

View File

@@ -1,24 +1,23 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser'; import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { getOpenAIApi, authChat } from '@/service/utils/chat'; import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai'; import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { chatId, windowId, prompt } = req.body as { const { chatId, prompt } = req.body as {
prompt: ChatItemType; prompt: ChatItemType;
windowId: string;
chatId: string; chatId: string;
}; };
try { try {
if (!windowId || !chatId || !prompt) { if (!chatId || !prompt) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
@@ -26,11 +25,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { chat, userApiKey } = await authChat(chatId); const { chat, userApiKey } = await authChat(chatId);
const model: ModelType = chat.modelId; const model: ModelSchema = chat.modelId;
// 读取对话内容 // 读取对话内容
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content; const prompts = [...chat.content, prompt];
prompts.push(prompt);
// 上下文长度过滤 // 上下文长度过滤
const maxContext = model.security.contextMaxLen; const maxContext = model.security.contextMaxLen;
@@ -49,6 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
content: item.value content: item.value
}) })
); );
// 如果有系统提示词,自动插入 // 如果有系统提示词,自动插入
if (model.systemPrompt) { if (model.systemPrompt) {
formatPrompts.unshift({ formatPrompts.unshift({

View File

@@ -1,19 +1,19 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { windowId } = req.query as { windowId: string }; const { chatId } = req.query as { chatId: string };
if (!windowId) { if (!chatId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
await connectToDatabase(); await connectToDatabase();
// 删除最一条数据库记录, 也就是预发送的那一条 // 删除最一条数据库记录, 也就是预发送的那一条
await ChatWindow.findByIdAndUpdate(windowId, { await Chat.findByIdAndUpdate(chatId, {
$pop: { content: 1 }, $pop: { content: 1 },
updateTime: Date.now() updateTime: Date.now()
}); });

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Chat } from '@/service/mongo'; import { connectToDatabase, Model, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools'; import { authToken } from '@/service/utils/tools';
import { ModelType } from '@/types/model'; import type { ModelSchema } from '@/types/mongoSchema';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -24,7 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase(); await connectToDatabase();
// 获取模型配置 // 获取模型配置
const model: ModelType | null = await Model.findOne({ const model = await Model.findOne<ModelSchema>({
_id: modelId, _id: modelId,
userId userId
}); });
@@ -38,11 +38,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
userId, userId,
modelId, modelId,
expiredTime: Date.now() + model.security.expiredTime, expiredTime: Date.now() + model.security.expiredTime,
loadAmount: model.security.maxLoadAmount loadAmount: model.security.maxLoadAmount,
updateTime: Date.now(),
content: []
}); });
jsonRes(res, { jsonRes(res, {
data: response._id data: response._id // 即聊天框的 ID
}); });
} catch (err) { } catch (err) {
jsonRes(res, { jsonRes(res, {

View File

@@ -1,9 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import type { ModelType } from '@/types/model'; import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { getOpenAIApi } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent } from '@/service/utils/tools';
@@ -18,35 +17,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase(); await connectToDatabase();
// 获取 chat 数据 const { chat, userApiKey } = await authChat(chatId);
const chat = await Chat.findById(chatId)
.populate({
path: 'modelId',
options: {
strictPopulate: false
}
})
.populate({
path: 'userId',
options: {
strictPopulate: false
}
});
if (!chat || !chat.modelId || !chat.userId) { const model = chat.modelId;
throw new Error('聊天已过期');
}
const model: ModelType = chat.modelId;
// 获取 user 的 apiKey
const user = chat.userId;
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
if (!userApiKey) {
throw new Error('缺少ApiKey, 无法请求');
}
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey); const chatAPI = getOpenAIApi(userApiKey);

View File

@@ -1,12 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
import type { ModelType } from '@/types/model'; import type { ChatPopulate } from '@/types/mongoSchema';
import type { InitChatResponse } from '@/api/response/chat';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { chatId, windowId } = req.query as { chatId: string; windowId?: string }; const { chatId } = req.query as { chatId: string };
if (!chatId) { if (!chatId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
@@ -15,16 +16,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase(); await connectToDatabase();
// 获取 chat 数据 // 获取 chat 数据
const chat = await Chat.findById(chatId).populate({ const chat = await Chat.findById<ChatPopulate>(chatId).populate({
path: 'modelId', path: 'modelId',
options: { options: {
strictPopulate: false strictPopulate: false
} }
}); });
// 安全校验 if (!chat) {
if (!chat || chat.loadAmount === 0 || chat.expiredTime < Date.now()) { throw new Error('聊天框不存在');
throw new Error('聊天框已过期');
} }
if (chat.loadAmount > 0) { if (chat.loadAmount > 0) {
@@ -38,38 +38,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
); );
} }
const model: ModelType = chat.modelId; const model = chat.modelId;
jsonRes<InitChatResponse>(res, {
/* 查找是否有记录 */ code: 201,
let history = null;
let responseId = windowId;
try {
history = await ChatWindow.findById(windowId);
} catch (error) {
error;
}
if (!history) {
// 没有记录,创建一个
const response = await ChatWindow.create({
chatId,
updateTime: Date.now(),
content: []
});
responseId = response._id;
}
jsonRes(res, {
data: { data: {
windowId: responseId, chatId: chat._id,
chatSite: { isExpiredTime: chat.loadAmount === 0 || chat.expiredTime <= Date.now(),
modelId: model._id, modelId: model._id,
name: model.name, name: model.name,
avatar: model.avatar, avatar: model.avatar,
secret: model.security, secret: model.security,
chatModel: model.service.chatModel chatModel: model.service.chatModel,
}, history: chat.content
history: history ? history.content : []
} }
}); });
} catch (err) { } catch (err) {

View File

@@ -1,24 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
/* 聊天内容存存储 */ /* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { windowId, prompts } = req.body as { const { chatId, prompts } = req.body as {
windowId: string; chatId: string;
prompts: ChatItemType[]; prompts: ChatItemType[];
}; };
if (!windowId || !prompts) { if (!chatId || !prompts) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
await connectToDatabase(); await connectToDatabase();
// 存入库 // 存入库
await ChatWindow.findByIdAndUpdate(windowId, { await Chat.findByIdAndUpdate(chatId, {
$push: { $push: {
content: { content: {
$each: prompts.map((item) => ({ $each: prompts.map((item) => ({

View File

@@ -8,7 +8,7 @@ import {
AccordionPanel, AccordionPanel,
AccordionIcon, AccordionIcon,
Flex, Flex,
Input, Divider,
IconButton IconButton
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
@@ -16,28 +16,30 @@ import { useChatStore } from '@/store/chat';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useScreen } from '@/hooks/useScreen'; import { useScreen } from '@/hooks/useScreen';
import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import { useCopyData } from '@/utils/tools';
const SlideBar = ({ const SlideBar = ({
name, name,
windowId,
chatId, chatId,
modelId,
resetChat, resetChat,
onClose onClose
}: { }: {
resetChat: () => void;
name?: string; name?: string;
windowId?: string;
chatId: string; chatId: string;
modelId: string;
resetChat: () => void;
onClose: () => void; onClose: () => void;
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { isPc } = useScreen(); const { copyData } = useCopyData();
const { myModels, getMyModels } = useUserStore(); const { myModels, getMyModels } = useUserStore();
const { chatHistory, removeChatHistoryByWindowId, generateChatWindow, updateChatHistory } = const { chatHistory, removeChatHistoryByWindowId, generateChatWindow, updateChatHistory } =
useChatStore(); useChatStore();
const { isSuccess } = useQuery(['init'], getMyModels); const { isSuccess } = useQuery(['init'], getMyModels);
const [hasReady, setHasReady] = useState(false); const [hasReady, setHasReady] = useState(false);
const [editHistoryId, setEditHistoryId] = useState<string>();
useEffect(() => { useEffect(() => {
setHasReady(true); setHasReady(true);
@@ -47,7 +49,7 @@ const SlideBar = ({
<> <>
{chatHistory.map((item) => ( {chatHistory.map((item) => (
<Flex <Flex
key={item.windowId} key={item.chatId}
alignItems={'center'} alignItems={'center'}
p={3} p={3}
borderRadius={'md'} borderRadius={'md'}
@@ -58,21 +60,15 @@ const SlideBar = ({
}} }}
fontSize={'xs'} fontSize={'xs'}
border={'1px solid transparent'} border={'1px solid transparent'}
{...(item.chatId === chatId && item.windowId === windowId {...(item.chatId === chatId
? { ? {
borderColor: 'rgba(255,255,255,0.5)', borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)' backgroundColor: 'rgba(255,255,255,0.1)'
} }
: {})} : {})}
onClick={() => { onClick={() => {
if ( if (item.chatId === chatId) return;
(item.chatId === chatId && item.windowId === windowId) || router.push(`/chat?chatId=${item.chatId}`);
editHistoryId === item.windowId
)
return;
router.push(
`/chat?chatId=${item.chatId}&windowId=${item.windowId}&timeStamp=${Date.now()}`
);
onClose(); onClose();
}} }}
> >
@@ -87,7 +83,7 @@ const SlideBar = ({
aria-label={'edit'} aria-label={'edit'}
size={'xs'} size={'xs'}
onClick={(e) => { onClick={(e) => {
removeChatHistoryByWindowId(item.windowId); removeChatHistoryByWindowId(item.chatId);
e.stopPropagation(); e.stopPropagation();
}} }}
/> />
@@ -107,6 +103,7 @@ const SlideBar = ({
color={'white'} color={'white'}
> >
{/* 新对话 */} {/* 新对话 */}
{getToken() && (
<Button <Button
w={'100%'} w={'100%'}
variant={'white'} variant={'white'}
@@ -117,6 +114,8 @@ const SlideBar = ({
> >
</Button> </Button>
)}
{/* 我的模型 & 历史记录 折叠框*/} {/* 我的模型 & 历史记录 折叠框*/}
<Box flex={'1 0 0'} h={0} overflowY={'auto'}> <Box flex={'1 0 0'} h={0} overflowY={'auto'}>
{isSuccess ? ( {isSuccess ? (
@@ -161,13 +160,11 @@ const SlideBar = ({
: {})} : {})}
onClick={async () => { onClick={async () => {
if (item.name === name) return; if (item.name === name) return;
router.push( router.push(`/chat?chatId=${await generateChatWindow(item._id)}`);
`/chat?chatId=${await generateChatWindow(item._id)}&timeStamp=${Date.now()}`
);
onClose(); onClose();
}} }}
> >
<ChatIcon mr={2} /> <MyIcon name="model" mr={2} fill={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}> <Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name} {item.name}
</Box> </Box>
@@ -177,9 +174,54 @@ const SlideBar = ({
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
) : ( ) : (
<>
<Box mb={4} textAlign={'center'}>
</Box>
<RenderHistory /> <RenderHistory />
</>
)} )}
</Box> </Box>
<Divider my={4} />
{/* 分享 */}
{getToken() && (
<Flex
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={async () => {
copyData(
`${location.origin}/chat?chatId=${await generateChatWindow(modelId)}`,
'已复制分享链接'
);
}}
>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</Flex>
)}
<Flex
mt={4}
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={async () => {
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
}}
>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</Flex>
</Flex> </Flex>
); );
}; };

View File

@@ -1,8 +1,15 @@
import React, { useCallback, useState, useRef, useMemo } from 'react'; import React, { useCallback, useState, useRef, useMemo } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Image from 'next/image'; import Image from 'next/image';
import { getInitChatSiteInfo, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat'; import {
import { ChatSiteItemType, ChatSiteType } from '@/types/chat'; getInitChatSiteInfo,
getChatSiteId,
postGPT3SendPrompt,
delLastMessage,
postSaveChat
} from '@/api/chat';
import type { InitChatResponse } from '@/api/response/chat';
import { ChatSiteItemType } from '@/types/chat';
import { import {
Textarea, Textarea,
Box, Box,
@@ -15,43 +22,68 @@ import {
DrawerContent DrawerContent
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon'; import Icon from '@/components/Iconfont';
import { useScreen } from '@/hooks/useScreen'; import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { OpenAiModelEnum } from '@/constants/model'; import { ChatModelNameEnum } from '@/constants/model';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { useChatStore } from '@/store/chat'; import { useChatStore } from '@/store/chat';
import { streamFetch } from '@/api/fetch'; import { streamFetch } from '@/api/fetch';
import SlideBar from './components/SlideBar'; import SlideBar from './components/SlideBar';
import { getToken } from '@/utils/user';
const Markdown = dynamic(() => import('@/components/Markdown')); const Markdown = dynamic(() => import('@/components/Markdown'));
const textareaMinH = '22px'; const textareaMinH = '22px';
const Chat = ({ interface ChatType extends InitChatResponse {
chatId, history: ChatSiteItemType[];
windowId, }
timeStamp
}: { const Chat = ({ chatId }: { chatId: string }) => {
chatId: string;
windowId?: string;
timeStamp: string;
}) => {
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const { isPc, media } = useScreen(); const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const [chatData, setChatData] = useState<ChatType>({
chatId: '',
modelId: '',
name: '',
avatar: '',
secret: {},
chatModel: '',
history: [],
isExpiredTime: false
}); // 聊天框整体数据
const ChatBox = useRef<HTMLDivElement>(null); const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null); const TextareaDom = useRef<HTMLTextAreaElement>(null);
const [chatSiteData, setChatSiteData] = useState<ChatSiteType>(); // 聊天框整体数据
const [chatList, setChatList] = useState<ChatSiteItemType[]>([]); // 对话内容
const [inputVal, setInputVal] = useState(''); // 输入的内容 const [inputVal, setInputVal] = useState(''); // 输入的内容
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]); const isChatting = useMemo(
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]); () => chatData.history[chatData.history.length - 1]?.status === 'loading',
const { setLoading } = useGlobalStore(); [chatData.history]
);
const chatWindowError = useMemo(() => {
if (chatData.history[chatData.history.length - 1]?.obj === 'Human') {
return {
text: '内容出现异常',
canDelete: true
};
}
if (chatData.isExpiredTime) {
return {
text: '聊天框已过期',
canDelete: false
};
}
return '';
}, [chatData]);
const { pushChatHistory } = useChatStore(); const { pushChatHistory } = useChatStore();
// 滚动到底部 // 滚动到底部
@@ -67,23 +99,20 @@ const Chat = ({
// 初始化聊天框 // 初始化聊天框
useQuery( useQuery(
['initData', timeStamp], ['init', chatId],
() => { () => {
setLoading(true); setLoading(true);
return getInitChatSiteInfo(chatId, windowId); return getInitChatSiteInfo(chatId);
}, },
{ {
onSuccess(res) { onSuccess(res) {
// 可能没有 windowId给它设置一下 setChatData({
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}&timeStamp=${timeStamp}`); ...res,
history: res.history.map((item) => ({
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item, ...item,
status: 'finish' status: 'finish'
})) }))
); });
scrollToBottom(); scrollToBottom();
}, },
onError(e: any) { onError(e: any) {
@@ -113,10 +142,18 @@ const Chat = ({
}, []); }, []);
// 重载对话 // 重载对话
const resetChat = useCallback(() => { const resetChat = useCallback(async () => {
router.push(`/chat?chatId=${chatId}&timeStamp=${Date.now()}`); if (!chatData) return;
try {
router.push(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
} catch (error: any) {
toast({
title: error?.message || '生成新对话失败',
status: 'warning'
});
}
onCloseSlider(); onCloseSlider();
}, [chatId, router, onCloseSlider]); }, [chatData, onCloseSlider, router, toast]);
// gpt3 方法 // gpt3 方法
const gpt3ChatPrompt = useCallback( const gpt3ChatPrompt = useCallback(
@@ -128,16 +165,17 @@ const Chat = ({
}); });
// 更新 AI 的内容 // 更新 AI 的内容
setChatList((state) => setChatData((state) => ({
state.map((item, index) => { ...state,
if (index !== state.length - 1) return item; history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return { return {
...item, ...item,
status: 'finish', status: 'finish',
value: response value: response
}; };
}) })
); }));
}, },
[chatId] [chatId]
); );
@@ -145,7 +183,6 @@ const Chat = ({
// chatGPT // chatGPT
const chatGPTPrompt = useCallback( const chatGPTPrompt = useCallback(
async (newChatList: ChatSiteItemType[]) => { async (newChatList: ChatSiteItemType[]) => {
if (!windowId) return;
const prompt = { const prompt = {
obj: newChatList[newChatList.length - 1].obj, obj: newChatList[newChatList.length - 1].obj,
value: newChatList[newChatList.length - 1].value value: newChatList[newChatList.length - 1].value
@@ -154,27 +191,27 @@ const Chat = ({
const res = await streamFetch({ const res = await streamFetch({
url: '/api/chat/chatGpt', url: '/api/chat/chatGpt',
data: { data: {
windowId,
prompt, prompt,
chatId chatId
}, },
onMessage: (text: string) => { onMessage: (text: string) => {
setChatList((state) => setChatData((state) => ({
state.map((item, index) => { ...state,
if (index !== state.length - 1) return item; history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return { return {
...item, ...item,
value: item.value + text value: item.value + text
}; };
}) })
); }));
} }
}); });
// 保存对话信息 // 保存对话信息
try { try {
await postSaveChat({ await postSaveChat({
windowId, chatId,
prompts: [ prompts: [
prompt, prompt,
{ {
@@ -193,17 +230,18 @@ const Chat = ({
} }
// 设置完成状态 // 设置完成状态
setChatList((state) => setChatData((state) => ({
state.map((item, index) => { ...state,
if (index !== state.length - 1) return item; history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return { return {
...item, ...item,
status: 'finish' status: 'finish'
}; };
}) })
); }));
}, },
[chatId, toast, windowId] [chatId, toast]
); );
/** /**
@@ -217,12 +255,12 @@ const Chat = ({
.split('\n') .split('\n')
.filter((val) => val) .filter((val) => val)
.join('\n\n'); .join('\n\n');
if (!chatSiteData?.modelId || !val || !ChatBox.current || isChatting) { if (!chatData?.modelId || !val || !ChatBox.current || isChatting) {
return; return;
} }
const newChatList: ChatSiteItemType[] = [ const newChatList: ChatSiteItemType[] = [
...chatList, ...chatData.history,
{ {
obj: 'Human', obj: 'Human',
value: val, value: val,
@@ -236,33 +274,37 @@ const Chat = ({
]; ];
// 插入内容 // 插入内容
setChatList(newChatList); setChatData((state) => ({
...state,
history: newChatList
}));
// 清空输入内容
resetInputVal(''); resetInputVal('');
scrollToBottom(); scrollToBottom();
const fnMap: { [key: string]: any } = { const fnMap: { [key: string]: any } = {
[OpenAiModelEnum.GPT35]: chatGPTPrompt, [ChatModelNameEnum.GPT35]: chatGPTPrompt,
[OpenAiModelEnum.GPT3]: gpt3ChatPrompt [ChatModelNameEnum.GPT3]: gpt3ChatPrompt
}; };
try { try {
/* 对长度进行限制 */ /* 对长度进行限制 */
const maxContext = chatSiteData.secret.contextMaxLen; const maxContext = chatData.secret.contextMaxLen;
const requestPrompt = const requestPrompt =
newChatList.length > maxContext + 1 newChatList.length > maxContext + 1
? newChatList.slice(newChatList.length - maxContext - 1, -1) ? newChatList.slice(newChatList.length - maxContext - 1, -1)
: newChatList.slice(0, -1); : newChatList.slice(0, -1);
if (typeof fnMap[chatSiteData.chatModel] === 'function') { if (typeof fnMap[chatData.chatModel] === 'function') {
await fnMap[chatSiteData.chatModel](requestPrompt); await fnMap[chatData.chatModel](requestPrompt);
} }
// 如果是 Human 第一次发送,插入历史记录 // 如果是 Human 第一次发送,插入历史记录
const humanChat = newChatList.filter((item) => item.obj === 'Human'); const humanChat = newChatList.filter((item) => item.obj === 'Human');
if (windowId && humanChat.length === 1) { if (humanChat.length === 1) {
pushChatHistory({ pushChatHistory({
chatId, chatId,
windowId,
title: humanChat[0].value title: humanChat[0].value
}); });
} }
@@ -276,34 +318,41 @@ const Chat = ({
resetInputVal(storeInput); resetInputVal(storeInput);
setChatList(newChatList.slice(0, newChatList.length - 2)); setChatData((state) => ({
...state,
history: newChatList.slice(0, newChatList.length - 2)
}));
} }
}, [ }, [
chatGPTPrompt,
chatList,
chatSiteData,
gpt3ChatPrompt,
inputVal, inputVal,
chatData.modelId,
chatData.history,
chatData.secret.contextMaxLen,
chatData.chatModel,
isChatting, isChatting,
resetInputVal, resetInputVal,
scrollToBottom, scrollToBottom,
toast, chatGPTPrompt,
gpt3ChatPrompt,
pushChatHistory,
chatId, chatId,
windowId, toast
pushChatHistory
]); ]);
// 重新编辑 // 重新编辑
const reEdit = useCallback(async () => { const reEdit = useCallback(async () => {
if (chatList[chatList.length - 1]?.obj !== 'Human') return; if (chatData.history[chatData.history.length - 1]?.obj !== 'Human') return;
// 删除数据库最后一句 // 删除数据库最后一句
await delLastMessage(windowId); await delLastMessage(chatId);
const val = chatList[chatList.length - 1].value; const val = chatData.history[chatData.history.length - 1].value;
resetInputVal(val); resetInputVal(val);
setChatList(chatList.slice(0, -1)); setChatData((state) => ({
}, [chatList, resetInputVal, windowId]); ...state,
history: state.history.slice(0, -1)
}));
}, [chatData.history, chatId, resetInputVal]);
return ( return (
<Flex h={'100%'} flexDirection={media('row', 'column')}> <Flex h={'100%'} flexDirection={media('row', 'column')}>
@@ -311,9 +360,9 @@ const Chat = ({
<Box flex={'0 0 250px'} w={0} h={'100%'}> <Box flex={'0 0 250px'} w={0} h={'100%'}>
<SlideBar <SlideBar
resetChat={resetChat} resetChat={resetChat}
name={chatSiteData?.name} name={chatData?.name}
windowId={windowId}
chatId={chatId} chatId={chatId}
modelId={chatData.modelId}
onClose={onCloseSlider} onClose={onCloseSlider}
/> />
</Box> </Box>
@@ -330,23 +379,18 @@ const Chat = ({
<Box onClick={onOpenSlider}> <Box onClick={onOpenSlider}>
<Icon name="icon-caidan" width={20} height={20}></Icon> <Icon name="icon-caidan" width={20} height={20}></Icon>
</Box> </Box>
<Box>{chatSiteData?.name}</Box> <Box>{chatData?.name}</Box>
</Flex> </Flex>
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}> <Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} /> <DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}> <DrawerContent maxWidth={'250px'}>
<SlideBar <SlideBar
resetChat={resetChat} resetChat={resetChat}
name={chatSiteData?.name} name={chatData?.name}
windowId={windowId}
chatId={chatId} chatId={chatId}
modelId={chatData.modelId}
onClose={onCloseSlider} onClose={onCloseSlider}
/> />
<DrawerFooter px={2} backgroundColor={'blackAlpha.800'}>
<Button variant="white" onClick={onCloseSlider}>
Cancel
</Button>
</DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
</Box> </Box>
@@ -359,7 +403,7 @@ const Chat = ({
> >
{/* 聊天内容 */} {/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}> <Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
{chatList.map((item, index) => ( {chatData.history.map((item, index) => (
<Box <Box
key={index} key={index}
py={media(9, 6)} py={media(9, 6)}
@@ -380,7 +424,7 @@ const Chat = ({
{item.obj === 'AI' ? ( {item.obj === 'AI' ? (
<Markdown <Markdown
source={item.value} source={item.value}
isChatting={isChatting && index === chatList.length - 1} isChatting={isChatting && index === chatData.history.length - 1}
/> />
) : ( ) : (
<Box whiteSpace={'pre-wrap'}>{item.value}</Box> <Box whiteSpace={'pre-wrap'}>{item.value}</Box>
@@ -398,14 +442,17 @@ const Chat = ({
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'} boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')} borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
> >
{lastWordHuman ? ( {!!chatWindowError ? (
<Box textAlign={'center'}> <Box textAlign={'center'}>
<Box color={'red'}></Box> <Box color={'red'}>{chatWindowError.text}</Box>
<Flex py={5} justifyContent={'center'}> <Flex py={5} justifyContent={'center'}>
<Button mr={20} onClick={resetChat} colorScheme={'green'}> {getToken() && <Button onClick={resetChat}></Button>}
{chatWindowError.canDelete && (
<Button ml={20} colorScheme={'green'} onClick={reEdit}>
</Button> </Button>
<Button onClick={reEdit}></Button> )}
</Flex> </Flex>
</Box> </Box>
) : ( ) : (
@@ -433,7 +480,7 @@ const Chat = ({
height={'22px'} height={'22px'}
lineHeight={'22px'} lineHeight={'22px'}
maxHeight={'150px'} maxHeight={'150px'}
maxLength={chatSiteData?.secret.contentMaxLen || -1} maxLength={chatData?.secret.contentMaxLen || -1}
overflowY={'auto'} overflowY={'auto'}
onChange={(e) => { onChange={(e) => {
const textarea = e.target; const textarea = e.target;
@@ -480,10 +527,8 @@ export default Chat;
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const chatId = context.query?.chatId || ''; const chatId = context.query?.chatId || '';
const windowId = context.query?.windowId || '';
const timeStamp = context.query?.timeStamp || `${Date.now()}`;
return { return {
props: { chatId, windowId, timeStamp } props: { chatId }
}; };
} }

View File

@@ -10,7 +10,7 @@ import { formatModelStatus, ModelStatusEnum, OpenAiList } from '@/constants/mode
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen'; import { useScreen } from '@/hooks/useScreen';
import ModelEditForm from './components/ModelEditForm'; import ModelEditForm from './components/ModelEditForm';
import Icon from '@/components/Icon'; import Icon from '@/components/Iconfont';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
const Training = dynamic(() => import('./components/Training')); const Training = dynamic(() => import('./components/Training'));

View File

@@ -20,7 +20,24 @@ const ChatSchema = new Schema({
// 剩余加载次数 // 剩余加载次数
type: Number, type: Number,
required: true required: true
},
updateTime: {
type: Number,
required: true
},
content: [
{
obj: {
type: String,
required: true,
enum: ['Human', 'AI', 'SYSTEM']
},
value: {
type: String,
required: true
} }
}
]
}); });
export const Chat = models['chat'] || model('chat', ChatSchema); export const Chat = models['chat'] || model('chat', ChatSchema);

View File

@@ -1,28 +0,0 @@
import { Schema, model, models } from 'mongoose';
const ChatWindowSchema = new Schema({
chatId: {
type: Schema.Types.ObjectId,
ref: 'chat',
required: true
},
updateTime: {
type: Number,
required: true
},
content: [
{
obj: {
type: String,
required: true,
enum: ['Human', 'AI', 'SYSTEM']
},
value: {
type: String,
required: true
}
}
]
});
export const ChatWindow = models['chatWindow'] || model('chatWindow', ChatWindowSchema);

View File

@@ -30,4 +30,3 @@ export * from './models/chat';
export * from './models/model'; export * from './models/model';
export * from './models/user'; export * from './models/user';
export * from './models/training'; export * from './models/training';
export * from './models/chatWindow';

View File

@@ -7,12 +7,12 @@ export interface ResponseType<T = any> {
data: T; data: T;
} }
export const jsonRes = ( export const jsonRes = <T = any>(
res: NextApiResponse, res: NextApiResponse,
props?: { props?: {
code?: number; code?: number;
message?: string; message?: string;
data?: any; data?: T;
error?: any; error?: any;
} }
) => { ) => {

View File

@@ -1,5 +1,6 @@
import { Configuration, OpenAIApi } from 'openai'; import { Configuration, OpenAIApi } from 'openai';
import { Chat } from '../mongo'; import { Chat } from '../mongo';
import type { ChatPopulate } from '@/types/mongoSchema';
export const getOpenAIApi = (apiKey: string) => { export const getOpenAIApi = (apiKey: string) => {
const configuration = new Configuration({ const configuration = new Configuration({
@@ -11,7 +12,7 @@ export const getOpenAIApi = (apiKey: string) => {
export const authChat = async (chatId: string) => { export const authChat = async (chatId: string) => {
// 获取 chat 数据 // 获取 chat 数据
const chat = await Chat.findById(chatId) const chat = await Chat.findById<ChatPopulate>(chatId)
.populate({ .populate({
path: 'modelId', path: 'modelId',
options: { options: {
@@ -26,7 +27,12 @@ export const authChat = async (chatId: string) => {
}); });
if (!chat || !chat.modelId || !chat.userId) { if (!chat || !chat.modelId || !chat.userId) {
return Promise.reject('聊天已过期'); return Promise.reject('模型不存在');
}
// 安全校验
if (chat.loadAmount === 0 || chat.expiredTime <= Date.now()) {
return Promise.reject('聊天框已过期');
} }
// 获取 user 的 apiKey // 获取 user 的 apiKey

View File

@@ -7,8 +7,8 @@ import { getChatSiteId } from '@/api/chat';
type Props = { type Props = {
chatHistory: HistoryItem[]; chatHistory: HistoryItem[];
pushChatHistory: (e: HistoryItem) => void; pushChatHistory: (e: HistoryItem) => void;
updateChatHistory: (windowId: string, title: string) => void; updateChatHistory: (chatId: string, title: string) => void;
removeChatHistoryByWindowId: (windowId: string) => void; removeChatHistoryByWindowId: (chatId: string) => void;
generateChatWindow: (modelId: string) => Promise<string>; generateChatWindow: (modelId: string) => Promise<string>;
}; };
export const useChatStore = create<Props>()( export const useChatStore = create<Props>()(
@@ -21,17 +21,17 @@ export const useChatStore = create<Props>()(
state.chatHistory = [item, ...state.chatHistory]; state.chatHistory = [item, ...state.chatHistory];
}); });
}, },
updateChatHistory(windowId: string, title: string) { updateChatHistory(chatId: string, title: string) {
set((state) => { set((state) => {
state.chatHistory = state.chatHistory.map((item) => ({ state.chatHistory = state.chatHistory.map((item) => ({
...item, ...item,
title: item.windowId === windowId ? title : item.title title: item.chatId === chatId ? title : item.title
})); }));
}); });
}, },
removeChatHistoryByWindowId(windowId: string) { removeChatHistoryByWindowId(chatId: string) {
set((state) => { set((state) => {
state.chatHistory = state.chatHistory.filter((item) => item.windowId !== windowId); state.chatHistory = state.chatHistory.filter((item) => item.chatId !== chatId);
}); });
}, },
generateChatWindow(modelId: string) { generateChatWindow(modelId: string) {

9
src/types/chat.d.ts vendored
View File

@@ -1,22 +1,15 @@
import type { ModelType } from './model'; import type { ModelType } from './model';
export interface ChatSiteType {
name: string;
avatar: string;
modelId: string;
chatModel: string;
secret: ModelType.security;
}
export type ChatItemType = { export type ChatItemType = {
obj: 'Human' | 'AI' | 'SYSTEM'; obj: 'Human' | 'AI' | 'SYSTEM';
value: string; value: string;
}; };
export type ChatSiteItemType = { export type ChatSiteItemType = {
status: 'loading' | 'finish'; status: 'loading' | 'finish';
} & ChatItemType; } & ChatItemType;
export type HistoryItem = { export type HistoryItem = {
chatId: string; chatId: string;
windowId: string;
title: string; title: string;
}; };

75
src/types/mongoSchema.d.ts vendored Normal file
View File

@@ -0,0 +1,75 @@
import type { ChatItemType } from './chat';
import { ModelStatusEnum, TrainingStatusEnum, ChatModelNameEnum } from '@/constants/model';
export type ServiceName = 'openai';
export interface UserModelSchema {
_id: string;
email: string;
password: string;
balance: number;
accounts: { type: 'openai'; value: string }[];
createTime: number;
}
export interface AuthCodeSchema {
_id: string;
email: string;
code: string;
type: 'register' | 'findPassword';
expiredTime: number;
}
export interface ModelSchema {
_id: string;
name: string;
avatar: string;
systemPrompt: string;
userId: string;
status: `${ModelStatusEnum}`;
updateTime: number;
trainingTimes: number;
service: {
company: ServiceName;
trainId: string;
chatModel: `${ChatModelNameEnum}`;
modelName: string;
};
security: {
domain: string[];
contextMaxLen: number;
contentMaxLen: number;
expiredTime: number;
maxLoadAmount: number;
};
}
export interface ModelPopulate extends ModelSchema {
userId: UserModelSchema;
}
export interface TrainingSchema {
_id: string;
serviceName: ServiceName;
tuneId: string;
modelId: string;
status: `${TrainingStatusEnum}`;
}
export interface TrainingPopulate extends TrainingSchema {
modelId: ModelSchema;
}
export interface ChatSchema {
_id: string;
userId: string;
modelId: string;
expiredTime: number;
loadAmount: number;
updateTime: number;
content: ChatItemType[];
}
export interface ChatPopulate extends ChatSchema {
userId: UserModelSchema;
modelId: ModelSchema;
}

View File

@@ -6,15 +6,20 @@ import { useToast } from '@/hooks/useToast';
*/ */
export const useCopyData = () => { export const useCopyData = () => {
const { toast } = useToast(); const { toast } = useToast();
return { return {
copyData: (data: string, title: string = '复制成功') => { copyData: async (data: string, title: string = '复制成功') => {
try { try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
} else {
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.value = data; textarea.value = data;
document.body.appendChild(textarea); document.body.appendChild(textarea);
textarea.select(); textarea.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(textarea); document.body.removeChild(textarea);
}
toast({ toast({
title, title,
status: 'success', status: 'success',