mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
feat: 修改chat的数据结构
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals"
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"react-hooks/rules-of-hooks": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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
2196
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
13
src/api/response/chat.d.ts
vendored
Normal 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;
|
||||||
|
};
|
1
src/components/Icon/icons/model.svg
Normal file
1
src/components/Icon/icons/model.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
1
src/components/Icon/icons/share.svg
Normal file
1
src/components/Icon/icons/share.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
@@ -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;
|
|
||||||
|
23
src/components/Iconfont/index.tsx
Normal file
23
src/components/Iconfont/index.tsx
Normal 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;
|
@@ -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',
|
||||||
|
@@ -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,
|
||||||
|
@@ -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';
|
||||||
|
@@ -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,拉个交流群大家一起聊聊。
|
||||||
|
@@ -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
|
||||||
|
@@ -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({
|
||||||
|
@@ -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()
|
||||||
});
|
});
|
||||||
|
@@ -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, {
|
||||||
|
@@ -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);
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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) => ({
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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'));
|
||||||
|
@@ -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);
|
||||||
|
@@ -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);
|
|
@@ -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';
|
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
|
@@ -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
|
||||||
|
@@ -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
9
src/types/chat.d.ts
vendored
@@ -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
75
src/types/mongoSchema.d.ts
vendored
Normal 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;
|
||||||
|
}
|
@@ -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',
|
||||||
|
Reference in New Issue
Block a user