doc gpt V0.2
This commit is contained in:
13
src/pages/404.tsx
Normal file
13
src/pages/404.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const NonePage = () => {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push('/model/list');
|
||||
}, [router]);
|
||||
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default NonePage;
|
||||
@@ -1,6 +1,46 @@
|
||||
import '@/styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import type { AppProps, NextWebVitalsMetric } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import Layout from '@/components/Layout';
|
||||
import { theme } from '@/constants/theme';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import '../styles/reset.scss';
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
cacheTime: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Doc GPT</title>
|
||||
<meta name="description" content="Generated by Doc GPT" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script src="/iconfont.js" async></script>
|
||||
</Head>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</ChakraProvider>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// export function reportWebVitals(metric: NextWebVitalsMetric) {
|
||||
// console.log(metric);
|
||||
// }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
@@ -9,5 +9,5 @@ export default function Document() {
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
110
src/pages/api/chat/chatGpt.ts
Normal file
110
src/pages/api/chat/chatGpt.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Encoding': 'none',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'text/event-stream'
|
||||
});
|
||||
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
|
||||
|
||||
try {
|
||||
if (!windowId || !chatId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { chat, userApiKey } = await authChat(chatId);
|
||||
|
||||
const model: ModelType = chat.modelId;
|
||||
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
// 读取对话内容
|
||||
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
||||
|
||||
// 长度过滤
|
||||
const maxContext = model.security.contextMaxLen;
|
||||
const filterPrompts =
|
||||
prompts.length > maxContext + 2
|
||||
? [prompts[0], ...prompts.slice(prompts.length - maxContext)]
|
||||
: prompts.slice(0, prompts.length);
|
||||
|
||||
// 格式化文本内容
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
})
|
||||
);
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey);
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: 1,
|
||||
// max_tokens: model.security.contentMaxLen,
|
||||
messages: formatPrompts,
|
||||
stream: true
|
||||
},
|
||||
openaiProxy
|
||||
);
|
||||
|
||||
// 截取字符串内容
|
||||
const reg = /{"content"(.*)"}/g;
|
||||
// @ts-ignore
|
||||
const match = chatResponse.data.match(reg);
|
||||
let AIResponse = '';
|
||||
if (match) {
|
||||
match.forEach((item: string, i: number) => {
|
||||
try {
|
||||
const json = JSON.parse(item);
|
||||
// 开头的换行忽略
|
||||
if (i === 0 && json.content?.startsWith('\n')) return;
|
||||
AIResponse += json.content;
|
||||
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
|
||||
content && res.write(`data: ${content}\n\n`);
|
||||
} catch (err) {
|
||||
err;
|
||||
}
|
||||
});
|
||||
}
|
||||
res.write(`data: [DONE]\n\n`);
|
||||
|
||||
// 存入库
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$push: {
|
||||
content: {
|
||||
obj: 'AI',
|
||||
value: AIResponse
|
||||
}
|
||||
},
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
res.end();
|
||||
} catch (err: any) {
|
||||
console.log(err?.response?.data || err);
|
||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$pop: { content: 1 },
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
28
src/pages/api/chat/delLastMessage.ts
Normal file
28
src/pages/api/chat/delLastMessage.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { windowId } = req.query as { windowId: string };
|
||||
|
||||
if (!windowId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$pop: { content: 1 },
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
53
src/pages/api/chat/generate.ts
Normal file
53
src/pages/api/chat/generate.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Chat } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ModelType } from '@/types/model';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query;
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取模型配置
|
||||
const model: ModelType | null = await Model.findOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('模型不存在');
|
||||
}
|
||||
|
||||
// 创建 chat 数据
|
||||
const response = await Chat.create({
|
||||
userId,
|
||||
modelId,
|
||||
expiredTime: Date.now() + model.security.expiredTime,
|
||||
loadAmount: model.security.maxLoadAmount
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: response._id
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
83
src/pages/api/chat/gpt3.ts
Normal file
83
src/pages/api/chat/gpt3.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { prompt, chatId } = req.body as { prompt: ChatItemType[]; chatId: string };
|
||||
|
||||
if (!prompt || !chatId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取 chat 数据
|
||||
const chat = await Chat.findById(chatId)
|
||||
.populate({
|
||||
path: 'modelId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
})
|
||||
.populate({
|
||||
path: 'userId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!chat || !chat.modelId || !chat.userId) {
|
||||
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
|
||||
const chatAPI = getOpenAIApi(userApiKey);
|
||||
|
||||
// prompt处理
|
||||
const formatPrompt = prompt.map((item) => `${item.value}\n\n###\n\n`).join('');
|
||||
|
||||
// 发送请求
|
||||
const response = await chatAPI.createCompletion(
|
||||
{
|
||||
model: model.service.modelName,
|
||||
prompt: formatPrompt,
|
||||
temperature: 0.5,
|
||||
max_tokens: model.security.contentMaxLen,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0.6,
|
||||
stop: ['###']
|
||||
},
|
||||
openaiProxy
|
||||
);
|
||||
|
||||
const responseMessage = response.data.choices[0]?.text;
|
||||
|
||||
jsonRes(res, {
|
||||
data: responseMessage
|
||||
});
|
||||
} catch (err: any) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
91
src/pages/api/chat/init.ts
Normal file
91
src/pages/api/chat/init.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
|
||||
import type { ModelType } from '@/types/model';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, windowId } = req.query as { chatId: string; windowId?: string };
|
||||
|
||||
if (!chatId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取 chat 数据
|
||||
const chat = await Chat.findById(chatId).populate({
|
||||
path: 'modelId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
});
|
||||
|
||||
// 安全校验
|
||||
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
|
||||
throw new Error('聊天框已过期');
|
||||
}
|
||||
|
||||
if (chat.loadAmount > 0) {
|
||||
await Chat.updateOne(
|
||||
{
|
||||
_id: chat._id
|
||||
},
|
||||
{
|
||||
$inc: { loadAmount: -1 }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const model: ModelType = chat.modelId;
|
||||
|
||||
/* 查找是否有记录 */
|
||||
let history = null;
|
||||
let responseId = windowId;
|
||||
try {
|
||||
history = await ChatWindow.findById(windowId);
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
|
||||
const defaultContent = model.systemPrompt
|
||||
? [
|
||||
{
|
||||
obj: 'SYSTEM',
|
||||
value: model.systemPrompt
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
if (!history) {
|
||||
// 没有记录,创建一个
|
||||
const response = await ChatWindow.create({
|
||||
chatId,
|
||||
updateTime: Date.now(),
|
||||
content: defaultContent
|
||||
});
|
||||
responseId = response._id;
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
windowId: responseId,
|
||||
chatSite: {
|
||||
modelId: model._id,
|
||||
name: model.name,
|
||||
avatar: model.avatar,
|
||||
secret: model.security,
|
||||
chatModel: model.service.chatModel
|
||||
},
|
||||
history: history ? history.content : defaultContent
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
43
src/pages/api/chat/preChat.ts
Normal file
43
src/pages/api/chat/preChat.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { authChat } from '@/service/utils/chat';
|
||||
|
||||
/* 聊天预请求,存储聊天内容 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { windowId, prompt, chatId } = req.body as {
|
||||
windowId: string;
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
if (!windowId || !prompt || !chatId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { chat } = await authChat(chatId);
|
||||
|
||||
// 长度校验
|
||||
const model: ModelType = chat.modelId;
|
||||
if (prompt.value.length > model.security.contentMaxLen) {
|
||||
throw new Error('输入内容超长');
|
||||
}
|
||||
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$push: { content: prompt },
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
type Data = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data>
|
||||
) {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
}
|
||||
75
src/pages/api/model/create.ts
Normal file
75
src/pages/api/model/create.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ModelStatusEnum, OpenAiList } from '@/constants/model';
|
||||
import { Model } from '@/service/models/model';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { name, serviceModelName, serviceModelCompany = 'openai' } = req.body;
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!name || !serviceModelName || !serviceModelCompany) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const modelItem = OpenAiList.find((item) => item.model === serviceModelName);
|
||||
|
||||
if (!modelItem) {
|
||||
throw new Error('模型错误');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 重名校验
|
||||
const authRepeatName = await Model.findOne({
|
||||
name,
|
||||
userId
|
||||
});
|
||||
if (authRepeatName) {
|
||||
throw new Error('模型名重复');
|
||||
}
|
||||
|
||||
// 上限校验
|
||||
const authCount = await Model.countDocuments({
|
||||
userId
|
||||
});
|
||||
if (authCount >= 5) {
|
||||
throw new Error('上限5个模型');
|
||||
}
|
||||
|
||||
// 创建模型
|
||||
const response = await Model.create({
|
||||
name,
|
||||
userId,
|
||||
status: ModelStatusEnum.running,
|
||||
service: {
|
||||
company: serviceModelCompany,
|
||||
trainId: modelItem.trainName,
|
||||
chatModel: modelItem.model,
|
||||
modelName: modelItem.model
|
||||
}
|
||||
});
|
||||
|
||||
// 根据 id 获取模型信息
|
||||
const model = await Model.findById(response._id);
|
||||
|
||||
jsonRes(res, {
|
||||
data: model
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
70
src/pages/api/model/del.ts
Normal file
70
src/pages/api/model/del.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { Chat, Model, Training, connectToDatabase } from '@/service/mongo';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { TrainingStatusEnum } from '@/constants/model';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { TrainingItemType } from '@/types/training';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query;
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 删除模型
|
||||
await Model.deleteOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
// 删除对应的聊天
|
||||
await Chat.deleteMany({
|
||||
modelId
|
||||
});
|
||||
|
||||
// 查看是否正在训练
|
||||
const training: TrainingItemType | null = await Training.findOne({
|
||||
modelId,
|
||||
status: TrainingStatusEnum.pending
|
||||
});
|
||||
|
||||
// 如果正在训练,需要删除openai上的相关信息
|
||||
if (training) {
|
||||
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||
// 获取训练记录
|
||||
const tuneRecord = await openai.retrieveFineTune(training.tuneId, openaiProxy);
|
||||
|
||||
// 删除训练文件
|
||||
openai.deleteFile(tuneRecord.data.training_files[0].id, openaiProxy);
|
||||
// 取消训练
|
||||
openai.cancelFineTune(training.tuneId, openaiProxy);
|
||||
}
|
||||
|
||||
// 删除对应训练记录
|
||||
await Training.deleteMany({
|
||||
modelId
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
47
src/pages/api/model/detail.tsx
Normal file
47
src/pages/api/model/detail.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { Model } from '@/service/models/model';
|
||||
import { ModelType } from '@/types/model';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
const { modelId } = req.query;
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 userId 获取模型信息
|
||||
const model: ModelType | null = await Model.findOne({
|
||||
userId,
|
||||
_id: modelId
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('模型不存在');
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: model
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
60
src/pages/api/model/getTrainings.ts
Normal file
60
src/pages/api/model/getTrainings.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Training } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import formidable from 'formidable';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { join } from 'path';
|
||||
import fs from 'fs';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import type { OpenAIApi } from 'openai';
|
||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
|
||||
// 关闭next默认的bodyParser处理方式
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false
|
||||
}
|
||||
};
|
||||
|
||||
/* 上传文件,开始微调 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
const { modelId } = req.query;
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
/* 获取 modelId 下的 training 记录 */
|
||||
const records = await Training.find({
|
||||
modelId
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: records
|
||||
});
|
||||
} catch (err: any) {
|
||||
/* 清除上传的文件,关闭训练记录 */
|
||||
// @ts-ignore
|
||||
if (openai) {
|
||||
// @ts-ignore
|
||||
uploadFileId && openai.deleteFile(uploadFileId);
|
||||
// @ts-ignore
|
||||
trainId && openai.cancelFineTune(trainId);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
35
src/pages/api/model/list.ts
Normal file
35
src/pages/api/model/list.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { Model } from '@/service/models/model';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 userId 获取模型信息
|
||||
const models = await Model.find({
|
||||
userId
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: models
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
101
src/pages/api/model/putTrainStatus.ts
Normal file
101
src/pages/api/model/putTrainStatus.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Training } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { TrainingItemType } from '@/types/training';
|
||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
|
||||
/* 更新训练状态 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取模型
|
||||
const model: ModelType | null = await Model.findById(modelId);
|
||||
|
||||
if (!model || model.status !== 'training') {
|
||||
throw new Error('模型不在训练中');
|
||||
}
|
||||
|
||||
// 查询正在训练中的训练记录
|
||||
const training: TrainingItemType | null = await Training.findOne({
|
||||
modelId,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
if (!training) {
|
||||
throw new Error('找不到训练记录');
|
||||
}
|
||||
|
||||
// 用户的 openai 实例
|
||||
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||
|
||||
// 获取 openai 的训练情况
|
||||
const { data } = await openai.retrieveFineTune(training.tuneId, openaiProxy);
|
||||
|
||||
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, openaiProxy);
|
||||
|
||||
// 更新模型
|
||||
await Model.findByIdAndUpdate(modelId, {
|
||||
status: ModelStatusEnum.running,
|
||||
updateTime: new Date(),
|
||||
service: {
|
||||
...model.service,
|
||||
trainId: data.fine_tuned_model, // 训练完后,再次训练和对话使用的 model 是一样的
|
||||
chatModel: data.fine_tuned_model
|
||||
}
|
||||
});
|
||||
// 更新训练数据
|
||||
await Training.findByIdAndUpdate(training._id, {
|
||||
status: TrainingStatusEnum.succeed
|
||||
});
|
||||
|
||||
return jsonRes(res, {
|
||||
data: '模型微调完成'
|
||||
});
|
||||
}
|
||||
|
||||
if (data.status === OpenAiTuneStatusEnum.cancelled) {
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, openaiProxy);
|
||||
|
||||
// 更新模型
|
||||
await Model.findByIdAndUpdate(modelId, {
|
||||
status: ModelStatusEnum.running,
|
||||
updateTime: new Date()
|
||||
});
|
||||
// 更新训练数据
|
||||
await Training.findByIdAndUpdate(training._id, {
|
||||
status: TrainingStatusEnum.canceled
|
||||
});
|
||||
|
||||
return jsonRes(res, {
|
||||
data: '模型微调取消'
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('模型还在训练中');
|
||||
} catch (err: any) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
127
src/pages/api/model/train.ts
Normal file
127
src/pages/api/model/train.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Training } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import formidable from 'formidable';
|
||||
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { join } from 'path';
|
||||
import fs from 'fs';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import type { OpenAIApi } from 'openai';
|
||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
|
||||
// 关闭next默认的bodyParser处理方式
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false
|
||||
}
|
||||
};
|
||||
|
||||
/* 上传文件,开始微调 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let openai: OpenAIApi, trainId: string, uploadFileId: string;
|
||||
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
const { modelId } = req.query;
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取模型的状态
|
||||
const model: ModelType | null = await Model.findById(modelId);
|
||||
|
||||
if (!model || model.status !== 'running') {
|
||||
throw new Error('模型正忙');
|
||||
}
|
||||
|
||||
// const trainingType = model.service.modelType
|
||||
const trainingType = model.service.trainId; // 目前都默认是 openai text-davinci-03
|
||||
|
||||
// 获取用户的 API Key 实例化后的对象
|
||||
openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||
|
||||
// 接收文件并保存
|
||||
const form = formidable({
|
||||
uploadDir: join(process.cwd(), 'public/trainData'),
|
||||
keepExtensions: true
|
||||
});
|
||||
|
||||
const { files } = await new Promise<{
|
||||
fields: formidable.Fields;
|
||||
files: formidable.Files;
|
||||
}>((resolve, reject) => {
|
||||
form.parse(req, (err, fields, files) => {
|
||||
if (err) return reject(err);
|
||||
resolve({ fields, files });
|
||||
});
|
||||
});
|
||||
const file = files.file;
|
||||
|
||||
// 上传文件
|
||||
// @ts-ignore
|
||||
const uploadRes = await openai.createFile(
|
||||
// @ts-ignore
|
||||
fs.createReadStream(file.filepath),
|
||||
'fine-tune',
|
||||
openaiProxy
|
||||
);
|
||||
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
|
||||
|
||||
// 开始训练
|
||||
const trainRes = await openai.createFineTune(
|
||||
{
|
||||
training_file: uploadFileId,
|
||||
model: trainingType,
|
||||
suffix: model.name
|
||||
},
|
||||
openaiProxy
|
||||
);
|
||||
|
||||
trainId = trainRes.data.id; // 记录训练 ID
|
||||
|
||||
// 创建训练记录
|
||||
await Training.create({
|
||||
serviceName: 'openai',
|
||||
tuneId: trainId,
|
||||
status: TrainingStatusEnum.pending,
|
||||
modelId
|
||||
});
|
||||
|
||||
// 修改模型状态
|
||||
await Model.findByIdAndUpdate(modelId, {
|
||||
$inc: {
|
||||
trainingTimes: +1
|
||||
},
|
||||
updateTime: new Date(),
|
||||
status: ModelStatusEnum.training
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: 'start training'
|
||||
});
|
||||
} catch (err: any) {
|
||||
/* 清除上传的文件,关闭训练记录 */
|
||||
// @ts-ignore
|
||||
if (openai) {
|
||||
// @ts-ignore
|
||||
uploadFileId && openai.deleteFile(uploadFileId, openaiProxy);
|
||||
// @ts-ignore
|
||||
trainId && openai.cancelFineTune(trainId, openaiProxy);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
49
src/pages/api/model/update.ts
Normal file
49
src/pages/api/model/update.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { Model } from '@/service/models/model';
|
||||
import type { ModelUpdateParams } from '@/types/model';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { name, service, security, systemPrompt } = req.body as ModelUpdateParams;
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权操作');
|
||||
}
|
||||
|
||||
if (!name || !service || !security || !modelId) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 更新模型
|
||||
await Model.updateOne(
|
||||
{
|
||||
_id: modelId,
|
||||
userId
|
||||
},
|
||||
{
|
||||
name,
|
||||
service,
|
||||
systemPrompt,
|
||||
security
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/pages/api/test.ts
Normal file
24
src/pages/api/test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
if (req.method !== 'GET') return;
|
||||
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Encoding': 'none',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'text/event-stream'
|
||||
});
|
||||
|
||||
let val = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
console.log('发送消息', val);
|
||||
res.write(`data: ${val++}\n\n`);
|
||||
if (val > 30) {
|
||||
clearInterval(timer);
|
||||
res.write(`data: [DONE]\n\n`);
|
||||
res.end();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
24
src/pages/api/timer/clearAuthCode.ts
Normal file
24
src/pages/api/timer/clearAuthCode.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const authCode = await AuthCode.deleteMany({
|
||||
expiredTime: { $lt: Date.now() }
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
message: `删除了${authCode.deletedCount}条记录`
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
25
src/pages/api/timer/clearChatWindow.ts
Normal file
25
src/pages/api/timer/clearChatWindow.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||
|
||||
/* 定时删除那些不活跃的内容 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const response = await ChatWindow.deleteMany(
|
||||
{ $expr: { $lt: [{ $size: '$content' }, 5] } },
|
||||
// 使用 $pull 操作符删除数组中的元素
|
||||
{ $pull: { content: { $exists: true } } }
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
message: `删除了${response.deletedCount}条记录`
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
76
src/pages/api/timer/updateTraining.ts
Normal file
76
src/pages/api/timer/updateTraining.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Training, Model } from '@/service/mongo';
|
||||
import type { TrainingItemType } from '@/types/training';
|
||||
import { TrainingStatusEnum, ModelStatusEnum } from '@/constants/model';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getUserOpenaiKey } from '@/service/utils/tools';
|
||||
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
||||
import { sendTrainSucceed } from '@/service/utils/sendEmail';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
// 查询正在训练中的训练记录
|
||||
const trainingRecords: TrainingItemType[] = await Training.find({
|
||||
status: TrainingStatusEnum.pending
|
||||
});
|
||||
|
||||
const openai = getOpenAIApi(await getUserOpenaiKey('63f9a14228d2a688d8dc9e1b'));
|
||||
|
||||
const response = await Promise.all(
|
||||
trainingRecords.map(async (item) => {
|
||||
const { data } = await openai.retrieveFineTune(item.tuneId, openaiProxy);
|
||||
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, openaiProxy);
|
||||
|
||||
const model = await Model.findById(item.modelId).populate({
|
||||
path: 'userId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('模型不存在');
|
||||
}
|
||||
|
||||
// 更新模型
|
||||
await Model.findByIdAndUpdate(item.modelId, {
|
||||
status: ModelStatusEnum.running,
|
||||
updateTime: new Date(),
|
||||
service: {
|
||||
...model.service,
|
||||
trainId: data.fine_tuned_model, // 训练完后,再次训练和对话使用的 model 是一样的
|
||||
chatModel: data.fine_tuned_model
|
||||
}
|
||||
});
|
||||
// 更新训练数据
|
||||
await Training.findByIdAndUpdate(item._id, {
|
||||
status: TrainingStatusEnum.succeed
|
||||
});
|
||||
|
||||
// 发送邮件通知
|
||||
await sendTrainSucceed(model.userId.email as string, model.name);
|
||||
return 'succeed';
|
||||
}
|
||||
return 'pending';
|
||||
})
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: `${response.length}个训练线程,${
|
||||
response.filter((item) => item === 'succeed').length
|
||||
}个完成`
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
47
src/pages/api/user/loginByPassword.ts
Normal file
47
src/pages/api/user/loginByPassword.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { User } from '@/service/models/user';
|
||||
import { generateToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 检测邮箱是否存在
|
||||
const authEmail = await User.findOne({
|
||||
email
|
||||
});
|
||||
if (!authEmail) {
|
||||
throw new Error('邮箱未注册');
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('密码错误');
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
token: generateToken(user._id),
|
||||
user
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
61
src/pages/api/user/register.ts
Normal file
61
src/pages/api/user/register.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { User } from '@/service/models/user';
|
||||
import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { generateToken } from '@/service/utils/tools';
|
||||
import { EmailTypeEnum } from '@/constants/common';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { email, code, password } = req.body;
|
||||
|
||||
if (!email || !code || !password) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 验证码校验
|
||||
const authCode = await AuthCode.findOne({
|
||||
email,
|
||||
code,
|
||||
type: EmailTypeEnum.register,
|
||||
expiredTime: { $gte: Date.now() }
|
||||
});
|
||||
|
||||
if (!authCode) {
|
||||
throw new Error('验证码错误');
|
||||
}
|
||||
|
||||
// 重名校验
|
||||
const authRepeat = await User.findOne({
|
||||
email
|
||||
});
|
||||
|
||||
if (authRepeat) {
|
||||
throw new Error('邮箱已被注册');
|
||||
}
|
||||
|
||||
const response = await User.create({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
// 根据 id 获取用户信息
|
||||
const user = await User.findById(response._id);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
token: generateToken(user._id),
|
||||
user
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
54
src/pages/api/user/sendEmail.ts
Normal file
54
src/pages/api/user/sendEmail.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { sendCode } from '@/service/utils/sendEmail';
|
||||
import { EmailTypeEnum } from '@/constants/common';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { email, type } = req.query;
|
||||
|
||||
if (!email || !type) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += Math.floor(Math.random() * 10);
|
||||
}
|
||||
|
||||
// 判断 1 分钟内是否有重复数据
|
||||
const authCode = await AuthCode.findOne({
|
||||
email,
|
||||
type,
|
||||
expiredTime: { $gte: Date.now() + 4 * 60 * 1000 } // 如果有一个记录的过期时间,大于当前+4分钟,说明距离上次发送还没到1分钟。(因为默认创建时,过期时间是未来5分钟)
|
||||
});
|
||||
|
||||
if (authCode) {
|
||||
throw new Error('请勿频繁获取验证码');
|
||||
}
|
||||
|
||||
// 创建 auth 记录
|
||||
await AuthCode.create({
|
||||
email,
|
||||
type,
|
||||
code
|
||||
});
|
||||
|
||||
// 发送验证码
|
||||
await sendCode(email as string, code, type as `${EmailTypeEnum}`);
|
||||
|
||||
jsonRes(res, {
|
||||
message: '发送验证码成功'
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
36
src/pages/api/user/tokenLogin.ts
Normal file
36
src/pages/api/user/tokenLogin.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { User } from '@/service/models/user';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 id 获取用户信息
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('账号异常');
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: user
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
41
src/pages/api/user/update.ts
Normal file
41
src/pages/api/user/update.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { User } from '@/service/models/user';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
|
||||
/* 更新一些基本信息 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { accounts } = req.body as UserUpdateParams;
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 更新对应的记录
|
||||
await User.updateOne(
|
||||
{
|
||||
_id: userId
|
||||
},
|
||||
{
|
||||
// 限定字段
|
||||
...(accounts ? { accounts } : {})
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
59
src/pages/api/user/updatePasswordByCode.ts
Normal file
59
src/pages/api/user/updatePasswordByCode.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { User } from '@/service/models/user';
|
||||
import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { generateToken } from '@/service/utils/tools';
|
||||
import { EmailTypeEnum } from '@/constants/common';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { email, code, password } = req.body;
|
||||
|
||||
if (!email || !code || !password) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 验证码校验
|
||||
const authCode = await AuthCode.findOne({
|
||||
email,
|
||||
code,
|
||||
type: EmailTypeEnum.findPassword,
|
||||
expiredTime: { $gte: Date.now() }
|
||||
});
|
||||
|
||||
if (!authCode) {
|
||||
throw new Error('验证码错误');
|
||||
}
|
||||
|
||||
// 更新对应的记录
|
||||
await User.updateOne(
|
||||
{
|
||||
email
|
||||
},
|
||||
{
|
||||
password
|
||||
}
|
||||
);
|
||||
|
||||
// 根据 email 获取用户信息
|
||||
const user = await User.findOne({
|
||||
email
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
token: generateToken(user._id),
|
||||
user
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
390
src/pages/chat/index.tsx
Normal file
390
src/pages/chat/index.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
getInitChatSiteInfo,
|
||||
postGPT3SendPrompt,
|
||||
getChatGPTSendEvent,
|
||||
postChatGptPrompt,
|
||||
delLastMessage
|
||||
} from '@/api/chat';
|
||||
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
|
||||
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Icon from '@/components/Icon';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { OpenAiModelEnum } from '@/constants/model';
|
||||
|
||||
const Chat = () => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { media } = useScreen();
|
||||
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
|
||||
const ChatBox = useRef<HTMLDivElement>(null);
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [chatSiteData, setChatSiteData] = useState<ChatSiteType>(); // 聊天框整体数据
|
||||
const [chatList, setChatList] = useState<ChatSiteItemType[]>([]); // 对话内容
|
||||
const [inputVal, setInputVal] = useState(''); // 输入的内容
|
||||
|
||||
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
|
||||
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
|
||||
const { Loading } = useLoading();
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback(() => {
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
ChatBox.current &&
|
||||
ChatBox.current.scrollTo({
|
||||
top: ChatBox.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// 初始化聊天框
|
||||
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), {
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
|
||||
|
||||
setChatSiteData(res.chatSite);
|
||||
setChatList(
|
||||
res.history.map((item) => ({
|
||||
...item,
|
||||
status: 'finish'
|
||||
}))
|
||||
);
|
||||
scrollToBottom();
|
||||
},
|
||||
onError() {
|
||||
toast({
|
||||
title: '初始化异常',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// gpt3 方法
|
||||
const gpt3ChatPrompt = useCallback(
|
||||
async (newChatList: ChatSiteItemType[]) => {
|
||||
// 请求内容
|
||||
const response = await postGPT3SendPrompt({
|
||||
prompt: newChatList,
|
||||
chatId: chatId as string
|
||||
});
|
||||
|
||||
// 更新 AI 的内容
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
value: response
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[chatId]
|
||||
);
|
||||
|
||||
// chatGPT
|
||||
const chatGPTPrompt = useCallback(
|
||||
async (newChatList: ChatSiteItemType[]) => {
|
||||
if (!windowId) return;
|
||||
/* 预请求,把消息存入库 */
|
||||
await postChatGptPrompt({
|
||||
windowId,
|
||||
prompt: newChatList[newChatList.length - 1],
|
||||
chatId
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const event = getChatGPTSendEvent(chatId, windowId);
|
||||
event.onmessage = ({ data }) => {
|
||||
if (data === '[DONE]') {
|
||||
event.close();
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
resolve('');
|
||||
} else if (data) {
|
||||
const msg = data.replace(/<br\/>/g, '\n');
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + msg
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
event.onerror = (err) => {
|
||||
console.error(err, '===');
|
||||
event.close();
|
||||
reject('对话出现错误');
|
||||
};
|
||||
});
|
||||
},
|
||||
[chatId, windowId]
|
||||
);
|
||||
|
||||
/**
|
||||
* 发送一个内容
|
||||
*/
|
||||
const sendPrompt = useCallback(async () => {
|
||||
const storeInput = inputVal;
|
||||
// 去除空行
|
||||
const val = inputVal
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((val) => val)
|
||||
.join('\n\n');
|
||||
if (!chatSiteData?.modelId || !val || !ChatBox.current || isChatting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...chatList,
|
||||
{
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatList(newChatList);
|
||||
setInputVal('');
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height = 22 + 'px';
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const fnMap: { [key: string]: any } = {
|
||||
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
|
||||
[OpenAiModelEnum.GPT3]: gpt3ChatPrompt
|
||||
};
|
||||
|
||||
try {
|
||||
/* 对长度进行限制 */
|
||||
const maxContext = chatSiteData.secret.contextMaxLen;
|
||||
const requestPrompt =
|
||||
newChatList.length > maxContext + 2
|
||||
? [newChatList[0], ...newChatList.slice(newChatList.length - maxContext - 1, -1)]
|
||||
: newChatList.slice(0, newChatList.length - 1);
|
||||
|
||||
if (typeof fnMap[chatSiteData.chatModel] === 'function') {
|
||||
await fnMap[chatSiteData.chatModel](requestPrompt);
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : '聊天已过期',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
|
||||
setInputVal(storeInput);
|
||||
|
||||
setChatList(newChatList.slice(0, newChatList.length - 2));
|
||||
}
|
||||
}, [
|
||||
chatGPTPrompt,
|
||||
chatList,
|
||||
chatSiteData,
|
||||
gpt3ChatPrompt,
|
||||
inputVal,
|
||||
isChatting,
|
||||
scrollToBottom,
|
||||
toast
|
||||
]);
|
||||
|
||||
// 重新编辑
|
||||
const reEdit = useCallback(async () => {
|
||||
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
|
||||
// 删除数据库最后一句
|
||||
delLastMessage(windowId);
|
||||
const val = chatList[chatList.length - 1].value;
|
||||
|
||||
setInputVal(val);
|
||||
|
||||
setChatList(chatList.slice(0, -1));
|
||||
|
||||
setTimeout(() => {
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height = val.split('\n').length * 22 + 'px';
|
||||
}
|
||||
}, 100);
|
||||
}, [chatList, windowId]);
|
||||
|
||||
return (
|
||||
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}>
|
||||
{/* 头部 */}
|
||||
<Flex
|
||||
px={4}
|
||||
h={'50px'}
|
||||
alignItems={'center'}
|
||||
backgroundColor={'white'}
|
||||
boxShadow={'0 5px 10px rgba(0,0,0,0.1)'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Box flex={1}>{chatSiteData?.name}</Box>
|
||||
{/* 重置按键 */}
|
||||
<Box cursor={'pointer'} onClick={() => router.replace(`/chat?chatId=${chatId}`)}>
|
||||
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
|
||||
</Box>
|
||||
{/* 滚动到底部按键 */}
|
||||
{/* 滚动到底部 */}
|
||||
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
|
||||
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
|
||||
<Icon
|
||||
name={'icon-xiangxiazhankai-xianxingyuankuang'}
|
||||
width={25}
|
||||
height={25}
|
||||
color={'#718096'}
|
||||
></Icon>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
{/* 聊天内容 */}
|
||||
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}>
|
||||
{chatList.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
py={media(9, 6)}
|
||||
px={media(4, 3)}
|
||||
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
|
||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
||||
>
|
||||
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
|
||||
<Box mr={4}>
|
||||
<Image
|
||||
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'}
|
||||
alt="/imgs/modelAvatar.png"
|
||||
width={30}
|
||||
height={30}
|
||||
></Image>
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} w={0} overflowX={'auto'}>
|
||||
<Markdown
|
||||
source={item.value}
|
||||
isChatting={isChatting && index === chatList.length - 1}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Box
|
||||
m={media('20px auto', '0 auto')}
|
||||
w={media('100vw', '100%')}
|
||||
maxW={'800px'}
|
||||
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
|
||||
>
|
||||
{lastWordHuman ? (
|
||||
<Box textAlign={'center'}>
|
||||
<Box color={'red'}>对话出现了异常</Box>
|
||||
<Flex py={5} justifyContent={'center'}>
|
||||
<Button
|
||||
mr={20}
|
||||
onClick={() => router.replace(`/chat?chatId=${chatId}`)}
|
||||
colorScheme={'green'}
|
||||
>
|
||||
重开对话
|
||||
</Button>
|
||||
<Button onClick={reEdit}>重新编辑最后一句</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
py={5}
|
||||
position={'relative'}
|
||||
boxShadow={'base'}
|
||||
overflow={'hidden'}
|
||||
borderRadius={media('md', 'none')}
|
||||
>
|
||||
{/* 输入框 */}
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
w={'100%'}
|
||||
pr={'45px'}
|
||||
py={0}
|
||||
border={'none'}
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
placeholder="提问"
|
||||
resize={'none'}
|
||||
value={inputVal}
|
||||
rows={1}
|
||||
height={'22px'}
|
||||
lineHeight={'22px'}
|
||||
maxHeight={'150px'}
|
||||
maxLength={chatSiteData?.secret.contentMaxLen || -1}
|
||||
overflowY={'auto'}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
setInputVal(textarea.value);
|
||||
|
||||
textarea.style.height = textarea.value.split('\n').length * 22 + 'px';
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
sendPrompt();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
}}
|
||||
/>
|
||||
{/* 发送和等待按键 */}
|
||||
<Box position={'absolute'} bottom={5} right={media('20px', '10px')}>
|
||||
{isChatting ? (
|
||||
<Image
|
||||
style={{ transform: 'translateY(4px)' }}
|
||||
src={'/icon/chatting.svg'}
|
||||
width={30}
|
||||
height={30}
|
||||
alt={''}
|
||||
/>
|
||||
) : (
|
||||
<Box cursor={'pointer'} onClick={sendPrompt}>
|
||||
<Icon name={'icon-fasong'} width={20} height={20} color={'#718096'}></Icon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Loading loading={!chatSiteData} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
@@ -1,123 +1,17 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import { Inter } from '@next/font/google'
|
||||
import styles from '@/styles/Home.module.css'
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Card, Text, Box, Heading, Flex } from '@chakra-ui/react';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { introPage } from '@/constants/common';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const Home = () => {
|
||||
const router = useRouter();
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.description}>
|
||||
<p>
|
||||
Get started by editing
|
||||
<code className={styles.code}>src/pages/index.tsx</code>
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{' '}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className={styles.vercelLogo}
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Card p={5} lineHeight={2}>
|
||||
<Markdown source={introPage} isChatting={false} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
<div className={styles.center}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
<div className={styles.thirteen}>
|
||||
<Image
|
||||
src="/thirteen.svg"
|
||||
alt="13"
|
||||
width={40}
|
||||
height={31}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Docs <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Learn <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Templates <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Discover and deploy boilerplate example Next.js projects.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Deploy <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Instantly deploy your Next.js site to a shareable URL
|
||||
with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Home;
|
||||
|
||||
193
src/pages/login/components/ForgetPasswordForm.tsx
Normal file
193
src/pages/login/components/ForgetPasswordForm.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useState, Dispatch, useCallback } from 'react';
|
||||
import {
|
||||
FormControl,
|
||||
Box,
|
||||
Input,
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
useToast,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { PageTypeEnum } from '../../../constants/user';
|
||||
import { postFindPassword } from '@/api/user';
|
||||
import { useSendCode } from '@/hooks/useSendCode';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
|
||||
interface Props {
|
||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||
loginSuccess: (e: ResLogin) => void;
|
||||
}
|
||||
|
||||
interface RegisterType {
|
||||
email: string;
|
||||
code: string;
|
||||
password: string;
|
||||
password2: string;
|
||||
}
|
||||
|
||||
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
const toast = useToast();
|
||||
const { mediaLgMd } = useScreen();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
trigger,
|
||||
formState: { errors }
|
||||
} = useForm<RegisterType>({
|
||||
mode: 'onBlur'
|
||||
});
|
||||
|
||||
const { codeSending, sendCodeText, sendCode, codeCountDown } = useSendCode();
|
||||
|
||||
const onclickSendCode = useCallback(async () => {
|
||||
const check = await trigger('email');
|
||||
if (!check) return;
|
||||
sendCode({
|
||||
email: getValues('email'),
|
||||
type: 'findPassword'
|
||||
});
|
||||
}, [getValues, sendCode, trigger]);
|
||||
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
const onclickFindPassword = useCallback(
|
||||
async ({ email, code, password }: RegisterType) => {
|
||||
setRequesting(true);
|
||||
try {
|
||||
loginSuccess(
|
||||
await postFindPassword({
|
||||
email,
|
||||
code,
|
||||
password
|
||||
})
|
||||
);
|
||||
toast({
|
||||
title: `密码已找回`,
|
||||
status: 'success',
|
||||
position: 'top'
|
||||
});
|
||||
} catch (error) {
|
||||
typeof error === 'string' &&
|
||||
toast({
|
||||
title: error,
|
||||
status: 'error',
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
setRequesting(false);
|
||||
},
|
||||
[loginSuccess, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||
找回 DocGPT 账号
|
||||
</Box>
|
||||
<form onSubmit={handleSubmit(onclickFindPassword)}>
|
||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||
<Input
|
||||
placeholder="邮箱"
|
||||
size={mediaLgMd}
|
||||
{...register('email', {
|
||||
required: '邮箱不能为空',
|
||||
pattern: {
|
||||
value: /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/,
|
||||
message: '邮箱错误'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.email && errors.email.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||
<Flex>
|
||||
<Input
|
||||
flex={1}
|
||||
placeholder="验证码"
|
||||
size={mediaLgMd}
|
||||
{...register('code', {
|
||||
required: '验证码不能为空'
|
||||
})}
|
||||
></Input>
|
||||
<Button
|
||||
ml={5}
|
||||
w={'145px'}
|
||||
maxW={'50%'}
|
||||
size={mediaLgMd}
|
||||
onClick={onclickSendCode}
|
||||
isDisabled={codeCountDown > 0}
|
||||
isLoading={codeSending}
|
||||
>
|
||||
{sendCodeText}
|
||||
</Button>
|
||||
</Flex>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.code && errors.code.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.password}>
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder="新密码"
|
||||
size={mediaLgMd}
|
||||
{...register('password', {
|
||||
required: '密码不能为空',
|
||||
minLength: {
|
||||
value: 4,
|
||||
message: '密码最少4位最多12位'
|
||||
},
|
||||
maxLength: {
|
||||
value: 12,
|
||||
message: '密码最少4位最多12位'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.password && errors.password.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.password2}>
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder="确认密码"
|
||||
size={mediaLgMd}
|
||||
{...register('password2', {
|
||||
validate: (val) => (getValues('password') === val ? true : '两次密码不一致')
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.password2 && errors.password2.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Box
|
||||
float={'right'}
|
||||
fontSize="sm"
|
||||
mt={2}
|
||||
color={'blue.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('login')}
|
||||
>
|
||||
去登录
|
||||
</Box>
|
||||
<Button
|
||||
type="submit"
|
||||
mt={8}
|
||||
w={'100%'}
|
||||
size={mediaLgMd}
|
||||
colorScheme="blue"
|
||||
isLoading={requesting}
|
||||
>
|
||||
找回密码
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterForm;
|
||||
134
src/pages/login/components/LoginForm.tsx
Normal file
134
src/pages/login/components/LoginForm.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useState, Dispatch, useCallback } from 'react';
|
||||
import { FormControl, Flex, Input, Button, FormErrorMessage, Box } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { PageTypeEnum } from '@/constants/user';
|
||||
import { postLogin } from '@/api/user';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
|
||||
interface Props {
|
||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||
loginSuccess: (e: ResLogin) => void;
|
||||
}
|
||||
|
||||
interface LoginFormType {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
const { toast } = useToast();
|
||||
const { mediaLgMd } = useScreen();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<LoginFormType>();
|
||||
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
const onclickLogin = useCallback(
|
||||
async ({ email, password }: LoginFormType) => {
|
||||
setRequesting(true);
|
||||
try {
|
||||
loginSuccess(
|
||||
await postLogin({
|
||||
email,
|
||||
password
|
||||
})
|
||||
);
|
||||
toast({
|
||||
title: '登录成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
typeof error === 'string' &&
|
||||
toast({
|
||||
title: error,
|
||||
status: 'error',
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
setRequesting(false);
|
||||
},
|
||||
[loginSuccess, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||
登录 DocGPT
|
||||
</Box>
|
||||
<form onSubmit={handleSubmit(onclickLogin)}>
|
||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||
<Input
|
||||
placeholder="邮箱"
|
||||
size={mediaLgMd}
|
||||
{...register('email', {
|
||||
required: '邮箱不能为空',
|
||||
pattern: {
|
||||
value: /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/,
|
||||
message: '邮箱错误'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.email && errors.email.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.password}>
|
||||
<Input
|
||||
type={'password'}
|
||||
size={mediaLgMd}
|
||||
placeholder="密码"
|
||||
{...register('password', {
|
||||
required: '密码不能为空',
|
||||
minLength: {
|
||||
value: 4,
|
||||
message: '密码最少4位最多12位'
|
||||
},
|
||||
maxLength: {
|
||||
value: 12,
|
||||
message: '密码最少4位最多12位'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.password && errors.password.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Flex align={'center'} justifyContent={'space-between'} mt={6} color={'blue.600'}>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('forgetPassword')}
|
||||
fontSize="sm"
|
||||
>
|
||||
忘记密码?
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('register')}
|
||||
fontSize="sm"
|
||||
>
|
||||
注册账号
|
||||
</Box>
|
||||
</Flex>
|
||||
<Button
|
||||
type="submit"
|
||||
mt={8}
|
||||
w={'100%'}
|
||||
size={mediaLgMd}
|
||||
colorScheme="blue"
|
||||
isLoading={requesting}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
193
src/pages/login/components/RegisterForm.tsx
Normal file
193
src/pages/login/components/RegisterForm.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useState, Dispatch, useCallback } from 'react';
|
||||
import {
|
||||
FormControl,
|
||||
Box,
|
||||
Input,
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
useToast,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { PageTypeEnum } from '@/constants/user';
|
||||
import { postRegister } from '@/api/user';
|
||||
import { useSendCode } from '@/hooks/useSendCode';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
|
||||
interface Props {
|
||||
loginSuccess: (e: ResLogin) => void;
|
||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||
}
|
||||
|
||||
interface RegisterType {
|
||||
email: string;
|
||||
password: string;
|
||||
password2: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
const toast = useToast();
|
||||
const { mediaLgMd } = useScreen();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
trigger,
|
||||
formState: { errors }
|
||||
} = useForm<RegisterType>({
|
||||
mode: 'onBlur'
|
||||
});
|
||||
|
||||
const { codeSending, sendCodeText, sendCode, codeCountDown } = useSendCode();
|
||||
|
||||
const onclickSendCode = useCallback(async () => {
|
||||
const check = await trigger('email');
|
||||
if (!check) return;
|
||||
sendCode({
|
||||
email: getValues('email'),
|
||||
type: 'register'
|
||||
});
|
||||
}, [getValues, sendCode, trigger]);
|
||||
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
|
||||
const onclickRegister = useCallback(
|
||||
async ({ email, password, code }: RegisterType) => {
|
||||
setRequesting(true);
|
||||
try {
|
||||
loginSuccess(
|
||||
await postRegister({
|
||||
email,
|
||||
code,
|
||||
password
|
||||
})
|
||||
);
|
||||
toast({
|
||||
title: `注册成功`,
|
||||
status: 'success',
|
||||
position: 'top'
|
||||
});
|
||||
} catch (error) {
|
||||
typeof error === 'string' &&
|
||||
toast({
|
||||
title: error,
|
||||
status: 'error',
|
||||
position: 'top'
|
||||
});
|
||||
}
|
||||
setRequesting(false);
|
||||
},
|
||||
[loginSuccess, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||
注册 DocGPT 账号
|
||||
</Box>
|
||||
<form onSubmit={handleSubmit(onclickRegister)}>
|
||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||
<Input
|
||||
placeholder="邮箱"
|
||||
size={mediaLgMd}
|
||||
{...register('email', {
|
||||
required: '邮箱不能为空',
|
||||
pattern: {
|
||||
value: /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/,
|
||||
message: '邮箱错误'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.email && errors.email.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||
<Flex>
|
||||
<Input
|
||||
flex={1}
|
||||
size={mediaLgMd}
|
||||
placeholder="验证码"
|
||||
{...register('code', {
|
||||
required: '验证码不能为空'
|
||||
})}
|
||||
></Input>
|
||||
<Button
|
||||
ml={5}
|
||||
w={'145px'}
|
||||
maxW={'50%'}
|
||||
size={mediaLgMd}
|
||||
onClick={onclickSendCode}
|
||||
isDisabled={codeCountDown > 0}
|
||||
isLoading={codeSending}
|
||||
>
|
||||
{sendCodeText}
|
||||
</Button>
|
||||
</Flex>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.code && errors.code.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.password}>
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder="密码"
|
||||
size={mediaLgMd}
|
||||
{...register('password', {
|
||||
required: '密码不能为空',
|
||||
minLength: {
|
||||
value: 4,
|
||||
message: '密码最少4位最多12位'
|
||||
},
|
||||
maxLength: {
|
||||
value: 12,
|
||||
message: '密码最少4位最多12位'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.password && errors.password.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mt={8} isInvalid={!!errors.password2}>
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder="确认密码"
|
||||
size={mediaLgMd}
|
||||
{...register('password2', {
|
||||
validate: (val) => (getValues('password') === val ? true : '两次密码不一致')
|
||||
})}
|
||||
></Input>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.password2 && errors.password2.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Box
|
||||
float={'right'}
|
||||
fontSize="sm"
|
||||
mt={2}
|
||||
color={'blue.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => setPageType('login')}
|
||||
>
|
||||
已有账号,去登录
|
||||
</Box>
|
||||
<Button
|
||||
type="submit"
|
||||
mt={8}
|
||||
w={'100%'}
|
||||
size={mediaLgMd}
|
||||
colorScheme="blue"
|
||||
isLoading={requesting}
|
||||
>
|
||||
确认注册
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterForm;
|
||||
7
src/pages/login/index.module.scss
Normal file
7
src/pages/login/index.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.loginPage {
|
||||
background: url('/icon/login-bg.svg') no-repeat;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
user-select: none;
|
||||
}
|
||||
86
src/pages/login/index.tsx
Normal file
86
src/pages/login/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import styles from './index.module.scss';
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { PageTypeEnum } from '@/constants/user';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
import ForgetPasswordForm from './components/ForgetPasswordForm';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const Login = () => {
|
||||
const router = useRouter();
|
||||
const { isPc } = useScreen();
|
||||
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
|
||||
const { setUserInfo } = useUserStore();
|
||||
|
||||
const loginSuccess = useCallback(
|
||||
(res: ResLogin) => {
|
||||
setUserInfo(res.user, res.token);
|
||||
router.push('/');
|
||||
},
|
||||
[router, setUserInfo]
|
||||
);
|
||||
|
||||
const map = {
|
||||
[PageTypeEnum.login]: {
|
||||
Component: <LoginForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
||||
img: '/icon/loginLeft.svg'
|
||||
},
|
||||
[PageTypeEnum.register]: {
|
||||
Component: <RegisterForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
||||
img: '/icon/loginLeft.svg'
|
||||
},
|
||||
[PageTypeEnum.forgetPassword]: {
|
||||
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
||||
img: '/icon/loginLeft.svg'
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={styles.loginPage} p={isPc ? '10vh 10vw' : 0}>
|
||||
<Flex
|
||||
maxW={'1240px'}
|
||||
m={'auto'}
|
||||
backgroundColor={'#fff'}
|
||||
height="100%"
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
p={10}
|
||||
borderRadius={isPc ? 'md' : 'none'}
|
||||
gap={5}
|
||||
>
|
||||
{isPc && (
|
||||
<Image
|
||||
src={map[pageType].img}
|
||||
order={pageType === PageTypeEnum.login ? 0 : 2}
|
||||
flex={'1 0 0'}
|
||||
w="0"
|
||||
maxW={'600px'}
|
||||
height={'100%'}
|
||||
maxH={'450px'}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box
|
||||
order={1}
|
||||
flex={`0 0 ${isPc ? '400px' : '100%'}`}
|
||||
height={'100%'}
|
||||
maxH={'450px'}
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
py={5}
|
||||
px={10}
|
||||
borderRadius={isPc ? 'md' : 'none'}
|
||||
>
|
||||
{map[pageType].Component}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
126
src/pages/model/components/CreateModel.tsx
Normal file
126
src/pages/model/components/CreateModel.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { Dispatch, useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
Button,
|
||||
useToast,
|
||||
Input,
|
||||
Select
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postCreateModel } from '@/api/model';
|
||||
import { ModelType } from '@/types/model';
|
||||
import { OpenAiList } from '@/constants/model';
|
||||
|
||||
interface CreateFormType {
|
||||
name: string;
|
||||
serviceModelName: string;
|
||||
}
|
||||
|
||||
const CreateModel = ({
|
||||
isOpen,
|
||||
setCreateModelOpen,
|
||||
onSuccess
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setCreateModelOpen: Dispatch<boolean>;
|
||||
onSuccess: Dispatch<ModelType>;
|
||||
}) => {
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
const toast = useToast({
|
||||
duration: 2000,
|
||||
position: 'top'
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<CreateFormType>({
|
||||
defaultValues: {
|
||||
serviceModelName: OpenAiList[0].model
|
||||
}
|
||||
});
|
||||
|
||||
const handleCreateModel = useCallback(
|
||||
async (data: CreateFormType) => {
|
||||
setRequesting(true);
|
||||
try {
|
||||
const res = await postCreateModel(data);
|
||||
toast({
|
||||
title: '创建成功',
|
||||
status: 'success'
|
||||
});
|
||||
onSuccess(res);
|
||||
setCreateModelOpen(false);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err.message || '出现了意外',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setRequesting(false);
|
||||
},
|
||||
[onSuccess, setCreateModelOpen, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={() => setCreateModelOpen(false)}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>创建模型</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<FormControl mb={8} isInvalid={!!errors.name}>
|
||||
<Input
|
||||
placeholder="模型名称"
|
||||
{...register('name', {
|
||||
required: '模型名不能为空'
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.name && errors.name.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.serviceModelName}>
|
||||
<Select
|
||||
placeholder="选择基础模型类型"
|
||||
{...register('serviceModelName', {
|
||||
required: '底层模型不能为空'
|
||||
})}
|
||||
>
|
||||
{OpenAiList.map((item) => (
|
||||
<option key={item.model} value={item.model}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.serviceModelName && errors.serviceModelName.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button mr={3} colorScheme={'gray'} onClick={() => setCreateModelOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={requesting} onClick={handleSubmit(handleCreateModel)}>
|
||||
确认创建
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateModel;
|
||||
193
src/pages/model/components/ModelEditForm.tsx
Normal file
193
src/pages/model/components/ModelEditForm.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Grid, Box, Card, Flex, Button, FormControl, Input, Textarea } from '@chakra-ui/react';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { putModelById } from '@/api/model';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const ModelEditForm = ({ model }: { model: ModelType }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<ModelType>({
|
||||
defaultValues: model
|
||||
});
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { toast } = useToast();
|
||||
const { isPc } = useScreen();
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: ModelType) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await putModelById(data._id, {
|
||||
name: data.name,
|
||||
systemPrompt: data.systemPrompt,
|
||||
service: data.service,
|
||||
security: data.security
|
||||
});
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
toast({
|
||||
title: err as string,
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, toast]
|
||||
);
|
||||
const submitError = useCallback(() => {
|
||||
// deep search message
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj) return '提交表单错误';
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
toast({
|
||||
title: deepSearch(errors),
|
||||
status: 'error',
|
||||
duration: 4000,
|
||||
isClosable: true
|
||||
});
|
||||
}, [errors, toast]);
|
||||
|
||||
return (
|
||||
<Grid gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
||||
<Card p={4}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
修改模型信息
|
||||
</Box>
|
||||
<Button onClick={handleSubmit(onclickSave, submitError)}>保存</Button>
|
||||
</Flex>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>展示名称:</Box>
|
||||
<Input
|
||||
{...register('name', {
|
||||
required: '展示名称不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>对话模型:</Box>
|
||||
<Box>{model.service.modelName}</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
<Textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
{...register('systemPrompt')}
|
||||
placeholder={'系统的提示词,会在进入聊天时放置在第一句,用于限定模型的聊天范围'}
|
||||
/>
|
||||
</FormControl>
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
安全策略
|
||||
</Box>
|
||||
<FormControl mt={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 120px'}>单句最大长度:</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'number'}
|
||||
{...register('security.contentMaxLen', {
|
||||
required: '单句长度不能为空',
|
||||
min: {
|
||||
value: 0,
|
||||
message: '单句长度最小为0'
|
||||
},
|
||||
max: {
|
||||
value: 4000,
|
||||
message: '单句长度最长为4000'
|
||||
},
|
||||
valueAsNumber: true
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 120px'}>上下文最大长度:</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'number'}
|
||||
{...register('security.contextMaxLen', {
|
||||
required: '上下文长度不能为空',
|
||||
min: {
|
||||
value: 1,
|
||||
message: '上下文长度最小为5'
|
||||
},
|
||||
max: {
|
||||
value: 400000,
|
||||
message: '上下文长度最长为 400000'
|
||||
},
|
||||
valueAsNumber: true
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 120px'}>聊天过期时间:</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'number'}
|
||||
{...register('security.expiredTime', {
|
||||
required: '聊天过期时间不能为空',
|
||||
min: {
|
||||
value: 0.1,
|
||||
message: '聊天过期时间最小为0.1小时'
|
||||
},
|
||||
max: {
|
||||
value: 999999,
|
||||
message: '聊天过期时间最长为 999999 小时'
|
||||
},
|
||||
valueAsNumber: true
|
||||
})}
|
||||
></Input>
|
||||
<Box ml={3}>小时</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5} pb={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 130px'}>聊天最大加载次数:</Box>
|
||||
<Box flex={1}>
|
||||
<Input
|
||||
type={'number'}
|
||||
{...register('security.maxLoadAmount', {
|
||||
required: '聊天最大加载次数不能为空',
|
||||
max: {
|
||||
value: 999999,
|
||||
message: '聊天最大加载次数最小为 999999 次'
|
||||
},
|
||||
valueAsNumber: true
|
||||
})}
|
||||
></Input>
|
||||
<Box fontSize={'sm'} color={'blackAlpha.400'} position={'absolute'}>
|
||||
设置为-1代表不限制次数
|
||||
</Box>
|
||||
</Box>
|
||||
<Box ml={3}>次</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelEditForm;
|
||||
76
src/pages/model/components/ModelPhoneList.tsx
Normal file
76
src/pages/model/components/ModelPhoneList.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Flex, Heading, Tag } from '@chakra-ui/react';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { formatModelStatus } from '@/constants/model';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ModelPhoneList = ({
|
||||
models,
|
||||
handlePreviewChat
|
||||
}: {
|
||||
models: ModelType[];
|
||||
handlePreviewChat: (_: string) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
|
||||
{models.map((model) => (
|
||||
<Box
|
||||
key={model._id}
|
||||
_notFirst={{ borderTop: '1px solid rgba(0,0,0,0.1)' }}
|
||||
px={6}
|
||||
py={3}
|
||||
backgroundColor={'white'}
|
||||
>
|
||||
<Flex alignItems={'flex-start'}>
|
||||
<Box flex={'1 0 0'} w={0} fontSize={'lg'} fontWeight={'bold'}>
|
||||
{model.name}
|
||||
</Box>
|
||||
<Tag
|
||||
colorScheme={formatModelStatus[model.status].colorTheme}
|
||||
variant="solid"
|
||||
px={3}
|
||||
size={'md'}
|
||||
>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
</Flex>
|
||||
<Flex mt={5}>
|
||||
<Box flex={'0 0 100px'}>最后更新时间: </Box>
|
||||
<Box color={'blackAlpha.500'}>{dayjs(model.updateTime).format('YYYY-MM-DD HH:mm')}</Box>
|
||||
</Flex>
|
||||
<Flex mt={5}>
|
||||
<Box flex={'0 0 100px'}>AI模型: </Box>
|
||||
<Box color={'blackAlpha.500'}>{model.service.modelName}</Box>
|
||||
</Flex>
|
||||
<Flex mt={5}>
|
||||
<Box flex={'0 0 100px'}>训练次数: </Box>
|
||||
<Box color={'blackAlpha.500'}>{model.trainingTimes}次</Box>
|
||||
</Flex>
|
||||
<Flex mt={5} justifyContent={'flex-end'}>
|
||||
<Button
|
||||
mr={3}
|
||||
variant={'outline'}
|
||||
w={'100px'}
|
||||
size={'sm'}
|
||||
onClick={() => handlePreviewChat(model._id)}
|
||||
>
|
||||
体验
|
||||
</Button>
|
||||
<Button
|
||||
size={'sm'}
|
||||
w={'100px'}
|
||||
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelPhoneList;
|
||||
120
src/pages/model/components/ModelTable.tsx
Normal file
120
src/pages/model/components/ModelTable.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Tag,
|
||||
Card,
|
||||
Box
|
||||
} from '@chakra-ui/react';
|
||||
import { formatModelStatus } from '@/constants/model';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ModelTable = ({
|
||||
models = [],
|
||||
handlePreviewChat
|
||||
}: {
|
||||
models: ModelType[];
|
||||
handlePreviewChat: (_: string) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const columns = [
|
||||
{
|
||||
title: '模型名',
|
||||
key: 'name',
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '最后更新时间',
|
||||
key: 'updateTime',
|
||||
render: (item: ModelType) => dayjs(item.updateTime).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
render: (item: ModelType) => (
|
||||
<Tag
|
||||
colorScheme={formatModelStatus[item.status].colorTheme}
|
||||
variant="solid"
|
||||
px={3}
|
||||
size={'md'}
|
||||
>
|
||||
{formatModelStatus[item.status].text}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'AI模型',
|
||||
key: 'service',
|
||||
render: (item: ModelType) => (
|
||||
<Box wordBreak={'break-all'} whiteSpace={'pre-wrap'} maxW={'200px'}>
|
||||
{item.service.modelName}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '训练次数',
|
||||
key: 'trainingTimes',
|
||||
dataIndex: 'trainingTimes'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'control',
|
||||
render: (item: ModelType) => (
|
||||
<>
|
||||
<Button mr={3} onClick={() => handlePreviewChat(item._id)}>
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
onClick={() => router.push(`/model/detail?modelId=${item._id}`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Card py={3}>
|
||||
<TableContainer>
|
||||
<Table variant={'simple'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
{columns.map((item) => (
|
||||
<Th key={item.key}>{item.title}</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{models.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
{columns.map((col) => (
|
||||
<Td key={col.key}>
|
||||
{col.render
|
||||
? col.render(item)
|
||||
: !!col.dataIndex
|
||||
? // @ts-ignore nextline
|
||||
item[col.dataIndex]
|
||||
: ''}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelTable;
|
||||
70
src/pages/model/components/Training.tsx
Normal file
70
src/pages/model/components/Training.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { Box, Card, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
|
||||
import { ModelType } from '@/types/model';
|
||||
import { getModelTrainings } from '@/api/model';
|
||||
import type { TrainingItemType } from '@/types/training';
|
||||
|
||||
const Training = ({ model }: { model: ModelType }) => {
|
||||
const columns: {
|
||||
title: string;
|
||||
key: keyof TrainingItemType;
|
||||
dataIndex: string;
|
||||
}[] = [
|
||||
{
|
||||
title: '训练ID',
|
||||
key: 'tuneId',
|
||||
dataIndex: 'tuneId'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status'
|
||||
}
|
||||
];
|
||||
|
||||
const [records, setRecords] = useState<TrainingItemType[]>([]);
|
||||
|
||||
const loadTrainingRecords = useCallback(async (id: string) => {
|
||||
try {
|
||||
const res = await getModelTrainings(id);
|
||||
setRecords(res);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
model._id && loadTrainingRecords(model._id);
|
||||
}, [loadTrainingRecords, model]);
|
||||
|
||||
return (
|
||||
<Card p={4} h={'100%'}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
训练记录: {model.trainingTimes}次
|
||||
</Box>
|
||||
<TableContainer mt={4}>
|
||||
<Table variant={'simple'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
{columns.map((item) => (
|
||||
<Th key={item.key}>{item.title}</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{records.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
{columns.map((col) => (
|
||||
// @ts-ignore
|
||||
<Td key={col.key}>{item[col.dataIndex]}</Td>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Training;
|
||||
247
src/pages/model/detail.tsx
Normal file
247
src/pages/model/detail.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getModelById, delModelById, postTrainModel, putModelTrainingStatus } from '@/api/model';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { formatModelStatus, ModelStatusEnum, OpenAiList } from '@/constants/model';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import ModelEditForm from './components/ModelEditForm';
|
||||
import Icon from '@/components/Icon';
|
||||
import Training from './components/Training';
|
||||
|
||||
const ModelDetail = () => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认删除该模型?'
|
||||
});
|
||||
const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { modelId } = router.query as { modelId: string };
|
||||
const [model, setModel] = useState<ModelType>();
|
||||
|
||||
const canTrain = useMemo(() => {
|
||||
const openai = OpenAiList.find((item) => item.model === model?.service.modelName);
|
||||
return openai && openai.canTraining === true;
|
||||
}, [model]);
|
||||
|
||||
/* 加载模型数据 */
|
||||
const loadModel = useCallback(async () => {
|
||||
if (!modelId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getModelById(modelId as string);
|
||||
res.security.expiredTime /= 60 * 60 * 1000;
|
||||
setModel(res);
|
||||
|
||||
console.log(res);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [modelId, setLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadModel();
|
||||
}, [loadModel, modelId]);
|
||||
|
||||
/* 点击删除 */
|
||||
const handleDelModel = useCallback(async () => {
|
||||
if (!model) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await delModelById(model._id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success'
|
||||
});
|
||||
router.replace('/model/list');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, model, router, toast]);
|
||||
|
||||
/* 点前往聊天预览页 */
|
||||
const handlePreviewChat = useCallback(async () => {
|
||||
if (!model) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const chatId = await getChatSiteId(model._id);
|
||||
|
||||
router.push(`/chat?chatId=${chatId}`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, model, router]);
|
||||
|
||||
/* 上传数据集,触发微调 */
|
||||
const startTraining = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!modelId || !e.target.files || e.target.files?.length === 0) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const file = e.target.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await postTrainModel(modelId, formData);
|
||||
|
||||
toast({
|
||||
title: '开始训练,大约需要 30 分钟',
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
// 重新获取模型
|
||||
loadModel();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : '文件格式错误',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, loadModel, modelId, toast]
|
||||
);
|
||||
|
||||
/* 点击更新模型状态 */
|
||||
const handleClickUpdateStatus = useCallback(async () => {
|
||||
if (!model || model.status !== ModelStatusEnum.training) return;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await putModelTrainingStatus(model._id);
|
||||
loadModel();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, loadModel, model]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!model && (
|
||||
<>
|
||||
{/* 头部 */}
|
||||
<Card px={6} py={3}>
|
||||
{isPc ? (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
{model.name} 配置
|
||||
</Box>
|
||||
<Tag
|
||||
ml={2}
|
||||
variant="solid"
|
||||
colorScheme={formatModelStatus[model.status].colorTheme}
|
||||
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
|
||||
onClick={handleClickUpdateStatus}
|
||||
>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
<Box flex={1} />
|
||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||
对话体验
|
||||
</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
{model.name} 配置
|
||||
</Box>
|
||||
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
</Flex>
|
||||
<Box mt={4} textAlign={'right'}>
|
||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||
对话体验
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
{/* 基本信息编辑 */}
|
||||
<Box mt={5}>
|
||||
<ModelEditForm model={model} />
|
||||
</Box>
|
||||
{/* 其他配置 */}
|
||||
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
||||
<Training model={model} />
|
||||
<Card h={'100%'} p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
神奇操作
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>模型微调:</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
SelectFileDom.current?.click();
|
||||
}}
|
||||
title={!canTrain ? '' : '模型不支持微调'}
|
||||
isDisabled={!canTrain}
|
||||
>
|
||||
上传微调数据集
|
||||
</Button>
|
||||
<Flex
|
||||
as={'a'}
|
||||
href="/TrainingTemplate.jsonl"
|
||||
download
|
||||
ml={5}
|
||||
cursor={'pointer'}
|
||||
alignItems={'center'}
|
||||
color={'blue.500'}
|
||||
>
|
||||
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
|
||||
下载模板
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* 提示 */}
|
||||
<Box mt={3} py={3} color={'blackAlpha.500'}>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
每行包括一个 prompt 和一个 completion
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
|
||||
内容不都是同一个标点结尾,可以加一个空格打断相同性,
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>删除模型:</Box>
|
||||
<Button
|
||||
colorScheme={'red'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
openConfirm(() => {
|
||||
handleDelModel();
|
||||
});
|
||||
}}
|
||||
>
|
||||
删除模型
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
||||
</Box>
|
||||
<ConfirmChild />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelDetail;
|
||||
90
src/pages/model/list.tsx
Normal file
90
src/pages/model/list.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Button, Flex, Card } from '@chakra-ui/react';
|
||||
import { getMyModels } from '@/api/model';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import { ModelType } from '@/types/model';
|
||||
import CreateModel from './components/CreateModel';
|
||||
import { useRouter } from 'next/router';
|
||||
import ModelTable from './components/ModelTable';
|
||||
import ModelPhoneList from './components/ModelPhoneList';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const ModelList = () => {
|
||||
const { isPc } = useScreen();
|
||||
const router = useRouter();
|
||||
const [models, setModels] = useState<ModelType[]>([]);
|
||||
const [openCreateModel, setOpenCreateModel] = useState(false);
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
/* 加载模型 */
|
||||
const loadModels = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getMyModels();
|
||||
setModels(res);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading]);
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
}, [loadModels]);
|
||||
|
||||
/* 创建成功回调 */
|
||||
const createModelSuccess = useCallback((data: ModelType) => {
|
||||
setModels((state) => [data, ...state]);
|
||||
}, []);
|
||||
|
||||
/* 点前往聊天预览页 */
|
||||
const handlePreviewChat = useCallback(
|
||||
async (modelId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const chatId = await getChatSiteId(modelId);
|
||||
|
||||
router.push(`/chat?chatId=${chatId}`, undefined, {
|
||||
shallow: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[router, setLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
{/* 头部 */}
|
||||
<Card px={6} py={3}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'} fontSize={'xl'}>
|
||||
模型列表
|
||||
</Box>
|
||||
|
||||
<Button flex={'0 0 145px'} variant={'outline'} onClick={() => setOpenCreateModel(true)}>
|
||||
新建模型
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
{/* 表单 */}
|
||||
<Box mt={5} position={'relative'}>
|
||||
{isPc ? (
|
||||
<ModelTable models={models} handlePreviewChat={handlePreviewChat} />
|
||||
) : (
|
||||
<ModelPhoneList models={models} handlePreviewChat={handlePreviewChat} />
|
||||
)}
|
||||
</Box>
|
||||
{/* 创建弹窗 */}
|
||||
<CreateModel
|
||||
isOpen={openCreateModel}
|
||||
setCreateModelOpen={setOpenCreateModel}
|
||||
onSuccess={createModelSuccess}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelList;
|
||||
145
src/pages/number/setting.tsx
Normal file
145
src/pages/number/setting.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Select,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import { putUserInfo } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { UserType } from '@/types/user';
|
||||
|
||||
const NumberSetting = () => {
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
fields: accounts,
|
||||
append: appendAccount,
|
||||
remove: removeAccount
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'accounts'
|
||||
});
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserUpdateParams) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await putUserInfo(data);
|
||||
updateUserInfo(data);
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {}
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card px={6} py={4}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
账号信息
|
||||
</Box>
|
||||
<Box mt={6}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'}>邮箱:</Box>
|
||||
<Box>{userInfo?.email}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* <Box mt={6}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'}>余额:</Box>
|
||||
<Box>
|
||||
<strong>{userInfo?.balance}</strong> 元
|
||||
</Box>
|
||||
<Button size={'sm'} w={'80px'} ml={5}>
|
||||
充值
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box> */}
|
||||
</Card>
|
||||
<Card mt={6} px={6} py={4}>
|
||||
<Flex mb={5} justifyContent={'space-between'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
关联账号
|
||||
</Box>
|
||||
<Box>
|
||||
{accounts.length === 0 && (
|
||||
<Button
|
||||
mr={5}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
appendAccount({
|
||||
type: 'openai',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit(onclickSave)}>保存</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>账号类型</Th>
|
||||
<Th>值</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{accounts.map((item, i) => (
|
||||
<Tr key={item.id}>
|
||||
<Td minW={'200px'}>
|
||||
<Select
|
||||
{...register(`accounts.${i}.type`, {
|
||||
required: '类型不能为空'
|
||||
})}
|
||||
>
|
||||
<option value="openai">openai</option>
|
||||
</Select>
|
||||
</Td>
|
||||
<Td minW={'200px'} whiteSpace="pre-wrap" wordBreak={'break-all'}>
|
||||
<Input
|
||||
{...register(`accounts.${i}.value`, {
|
||||
required: '账号不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button onClick={() => removeAccount(i)}>删除</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberSetting;
|
||||
23
src/pages/training/dataList.tsx
Normal file
23
src/pages/training/dataList.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Card, Box, Flex, Button } from '@chakra-ui/react';
|
||||
|
||||
const TrainDataList = () => {
|
||||
return (
|
||||
<>
|
||||
<Card px={6} py={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
训练数据管理
|
||||
</Box>
|
||||
<Button variant={'outline'} mr={6}>
|
||||
导入数据
|
||||
</Button>
|
||||
<Button>插入一条数据</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
{/* 数据表 */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrainDataList;
|
||||
Reference in New Issue
Block a user