Compare commits

..

7 Commits
v2.9 ... v3.0

Author SHA1 Message Date
archer
5969f5e0c5 docs and login direct 2023-04-28 14:16:30 +08:00
archer
ca8e940c9b perf: image and auth 2023-04-28 14:01:27 +08:00
archer
75073a64fb fix: chat window auth 2023-04-28 13:21:56 +08:00
archer
5b9185159d feat: collection model 2023-04-28 13:10:12 +08:00
archer
08ae4073bd feat: model set avatar 2023-04-28 10:06:14 +08:00
archer
606105d633 perf: select file 2023-04-28 09:32:03 +08:00
archer
3b8e5d2738 feat: model share market 2023-04-27 23:41:42 +08:00
48 changed files with 914 additions and 262 deletions

View File

@@ -1,6 +1,6 @@
### Fast GPT V2.8.1
* 新增 - 暂停聊天。
* 优化 - 知识库升级,内容条数不上限!
* 优化 - 导入去重效果,可防止导出后的 csv 重复导入
* 优化 - 聊天框,电脑端复制删除图标
* 优化 - 聊天框,生成内容时,如果滚动条触底,则会自动向下滚动,不需要手动下滑
### Fast GPT V3.0
- 新增 - 模型共享市场,可以使用其他用户分享的模型。
- 新增 - 邀请好友注册功能
- 优化 - 选择文件,采用链式处理,避免卡死
- 修复 - 导入时分段问题

BIN
public/imgs/modelAvatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -406,12 +406,12 @@ function getVerbosityLevel() {
}
function info(msg) {
if (verbosity >= VerbosityLevel.INFOS) {
console.log(`Info: ${msg}`);
// console.log(`Info: ${msg}`);
}
}
function warn(msg) {
if (verbosity >= VerbosityLevel.WARNINGS) {
console.log(`Warning: ${msg}`);
// console.log(`Warning: ${msg}`);
}
}
function unreachable(msg) {
@@ -4206,7 +4206,7 @@ function loadScript(src, removeScriptElement = false) {
});
}
function deprecated(details) {
console.log("Deprecated API usage: " + details);
// console.log("Deprecated API usage: " + details);
}
let pdfDateStringRegex;
class PDFDateString {

View File

@@ -1008,12 +1008,12 @@ function getVerbosityLevel() {
}
function info(msg) {
if (verbosity >= VerbosityLevel.INFOS) {
console.log(`Info: ${msg}`);
// console.log(`Info: ${msg}`);
}
}
function warn(msg) {
if (verbosity >= VerbosityLevel.WARNINGS) {
console.log(`Warning: ${msg}`);
// console.log(`Warning: ${msg}`);
}
}
function unreachable(msg) {

View File

@@ -1,6 +1,6 @@
import { GET, POST, DELETE, PUT } from './request';
import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema';
import { ModelUpdateParams } from '@/types/model';
import { ModelUpdateParams, ShareModelItem } from '@/types/model';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
@@ -93,3 +93,19 @@ export const putModelDataById = (data: { dataId: string; a: string; q?: string }
*/
export const delOneModelData = (dataId: string) =>
DELETE(`/model/data/delModelDataById?dataId=${dataId}`);
/* 共享市场 */
/**
* 获取共享市场模型
*/
export const getShareModelList = (data: { searchText?: string } & RequestPaging) =>
POST(`/model/share/getModels`, data);
/**
* 获取收藏的模型
*/
export const getCollectionModels = () => GET<ShareModelItem[]>(`/model/share/getCollection`);
/**
* 收藏/取消收藏模型
*/
export const triggerModelCollection = (modelId: string) =>
POST<number>(`/model/share/collection?modelId=${modelId}`);

View File

@@ -6,6 +6,7 @@ export type InitChatResponse = {
modelId: string;
name: string;
avatar: string;
intro: string;
chatModel: ModelSchema.service.chatModel; // 对话模型名
modelName: ModelSchema.service.modelName; // 底层模型
history: ChatItemType[];

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682602070818" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2479" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M509.606998 143.114488c9.082866 0 17.327644 4.840238 20.996197 12.331863l97.262184 197.441814c5.613858 11.403724 16.663518 19.358907 29.438473 21.216207l223.738737 32.552393c8.420787 1.215688 15.604396 6.851035 18.23327 14.254655 2.520403 7.18361 0.595564 15.062044-5.084808 20.586874L730.253304 601.611947c-8.949836 8.751315-12.994965 21.171182-10.916631 33.370015l38.011732 222.060515c1.325182 7.737218-2.165316 15.426341-8.905834 19.978007-4.088108 2.741437-8.861832 4.155646-13.812587 4.155646-4.022617 0-7.999185-0.972141-11.425214-2.740414L528.149307 775.671215c-5.768377-3.006474-12.155854-4.552689-18.542308-4.552689-6.364965 0-12.727882 1.547239-18.518772 4.552689L296.254819 878.348736c-3.559059 1.855254-7.602142 2.828418-11.668761 2.828418-4.861728 0-9.723455-1.459235-13.546527-4.022617-6.961552-4.684696-10.475586-12.419867-9.127891-20.155039l38.011732-222.016513c2.078335-12.198833-1.988284-24.619724-10.939143-33.370015L125.02397 441.443038c-5.635347-5.492084-7.55814-13.348006-5.061272-20.453844 2.63092-7.481392 9.812483-13.116739 18.298761-14.332427l223.674269-32.552393c12.839423-1.857301 23.867594-9.813506 29.481452-21.216207l97.194646-197.396789C492.325403 147.965983 500.590648 143.114488 509.606998 143.114488M509.606998 104.904235c-24.043602 0-45.922912 13.226233-56.177464 33.95637L356.189863 336.302419l-223.674269 32.54216c-22.983457 3.304256-42.100864 18.718317-49.481971 39.659255-7.381108 21.048385-1.812275 44.23241 14.431687 60.033281l163.916257 160.125931-38.011732 222.016513c-3.868097 22.408359 6.03239 44.819788 25.458835 57.94676 10.69662 7.116071 23.204491 10.784624 35.757388 10.784624 10.298554 0 20.663622-2.475378 30.055526-7.337105l194.987926-102.7205L704.662463 912.072815c9.369392 4.861728 19.712971 7.337105 29.990035 7.337105 12.57541 0 25.082258-3.668553 35.778878-10.784624 19.426445-13.126972 29.305443-35.538401 25.460882-57.94676l-38.012755-222.016513 163.937746-160.125931c16.22145-15.812127 21.810748-38.984896 14.408151-60.033281-7.402597-20.940938-26.51898-36.353976-49.503461-39.659255L663.04767 336.302419l-97.240695-197.441814C555.619962 118.131491 533.695626 104.904235 509.606998 104.904235L509.606998 104.904235z" p-id="2480"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682602068431" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2339" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M335.008 916.629333c-35.914667 22.314667-82.88 10.773333-104.693333-25.557333a77.333333 77.333333 0 0 1-8.96-57.429333l46.485333-198.24a13.141333 13.141333 0 0 0-4.021333-12.864l-152.16-132.586667c-31.605333-27.52-35.253333-75.648-8.234667-107.733333a75.68 75.68 0 0 1 51.733333-26.752L354.848 339.2c4.352-0.362667 8.245333-3.232 10.026667-7.594667l76.938666-188.170666c16.032-39.2 60.618667-57.92 99.52-41.461334a76.309333 76.309333 0 0 1 40.832 41.461334l76.938667 188.16c1.781333 4.373333 5.674667 7.253333 10.026667 7.605333l199.712 16.277333c41.877333 3.413333 72.885333 40.458667 69.568 82.517334a76.938667 76.938667 0 0 1-26.08 51.978666l-152.16 132.586667c-3.541333 3.082667-5.141333 8.074667-4.021334 12.853333l46.485334 198.24c9.621333 41.013333-15.36 82.336-56.138667 92.224a75.285333 75.285333 0 0 1-57.525333-9.237333l-170.976-106.24a11.296 11.296 0 0 0-12.010667 0l-170.986667 106.24z" p-id="2340"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682599933100" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5707" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M750.592 668.7232a159.6416 159.6416 0 0 0-128.4608 64.9216l-269.568-138.24a159.3344 159.3344 0 0 0 17.0496-128.6656l261.12-136.704a159.4368 159.4368 0 1 0-31.1296-53.0432L341.1456 412.3648a159.7952 159.7952 0 1 0-32.256 229.7856l286.72 146.9952a159.7952 159.7952 0 1 0 154.88-120.4224z m0-542.72a98.3552 98.3552 0 1 1-98.3552 98.3552 98.4576 98.4576 0 0 1 98.3552-98.304z m-534.2208 484.352A98.3552 98.3552 0 1 1 314.7264 512a98.4576 98.4576 0 0 1-98.3552 98.3552zM750.592 926.72a98.3552 98.3552 0 1 1 98.3552-98.3552A98.4576 98.4576 0 0 1 750.592 926.72z" p-id="5708"></path></svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@@ -18,7 +18,10 @@ const map = {
withdraw: require('./icons/withdraw.svg').default,
dbModel: require('./icons/dbModel.svg').default,
history: require('./icons/history.svg').default,
stop: require('./icons/stop.svg').default
stop: require('./icons/stop.svg').default,
shareMarket: require('./icons/shareMarket.svg').default,
collectionLight: require('./icons/collectionLight.svg').default,
collectionSolid: require('./icons/collectionSolid.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -7,7 +7,8 @@ import { useQuery } from '@tanstack/react-query';
const unAuthPage: { [key: string]: boolean } = {
'/': true,
'/login': true
'/login': true,
'/model/share': true
};
const Auth = ({ children }: { children: JSX.Element }) => {
@@ -33,7 +34,9 @@ const Auth = ({ children }: { children: JSX.Element }) => {
{
onError(error) {
console.log('error->', error);
router.replace('/login');
router.replace(
`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`
);
toast();
},
onSettled() {

View File

@@ -20,12 +20,19 @@ const navbarList = [
link: '/',
activeLink: ['/']
},
{
label: '共享',
icon: 'shareMarket',
link: '/model/share',
activeLink: ['/model/share']
},
{
label: '模型',
icon: 'model',
link: '/model/list',
activeLink: ['/model/list', '/model/detail']
},
{
label: '账号',
icon: 'user',

View File

@@ -113,10 +113,10 @@ export const ModelVectorSearchModeMap: Record<
};
export const defaultModel: ModelSchema = {
_id: '',
userId: '',
_id: 'modelId',
userId: 'userId',
name: 'modelName',
avatar: '',
avatar: '/icon/logo.png',
status: ModelStatusEnum.pending,
updateTime: Date.now(),
systemPrompt: '',
@@ -124,6 +124,12 @@ export const defaultModel: ModelSchema = {
search: {
mode: ModelVectorSearchModeEnum.hightSimilarity
},
share: {
isShare: false,
isShareDetail: false,
intro: '',
collection: 0
},
service: {
chatModel: ModelNameEnum.GPT35,
modelName: ModelNameEnum.GPT35

View File

@@ -91,7 +91,7 @@ export const usePagination = <T = any,>({
useEffect(() => {
mutate(1);
}, [mutate]);
}, []);
return {
pageNum,

View File

@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 获取 model 数据
const { model } = await authModel(modelId, userId);
const { model } = await authModel({ modelId, userId, authUser: false, authOwner: false });
// 历史记录
let history: ChatItemType[] = [];
@@ -30,7 +30,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (chatId) {
// 获取 chat.content 数据
history = await Chat.aggregate([
{ $match: { _id: new mongoose.Types.ObjectId(chatId) } },
{
$match: {
_id: new mongoose.Types.ObjectId(chatId),
userId: new mongoose.Types.ObjectId(userId)
}
},
{ $unwind: '$content' },
{ $match: { 'content.deleted': false } },
{ $sort: { 'content._id': -1 } },
@@ -53,6 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelId: modelId,
name: model.name,
avatar: model.avatar,
intro: model.share.intro,
modelName: model.service.modelName,
chatModel: model.service.chatModel,
history

View File

@@ -27,9 +27,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
value: item.value
}));
await authModel({ modelId, userId, authOwner: false });
// 没有 chatId, 创建一个对话
if (!chatId) {
await authModel(modelId, userId);
const { _id } = await Chat.create({
userId,
modelId,

View File

@@ -4,6 +4,7 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import type { PgModelDataItemType } from '@/types/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -36,9 +37,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { model } = await authModel({
userId,
modelId,
authOwner: false
});
const where: any = [
['user_id', userId],
'AND',
...(model.share.isShareDetail ? [] : [['user_id', userId], 'AND']),
['model_id', modelId],
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
];

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateVector } from '@/service/events/generateVector';
import { ModelDataStatusEnum } from '@/constants/model';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -28,15 +29,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
await authModel({
userId,
modelId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 去重
const searchRes = await Promise.allSettled(
data.map(async ([q, a]) => {

View File

@@ -1,10 +1,11 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelDataSchema } from '@/types/mongoSchema';
import { generateVector } from '@/service/events/generateVector';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -28,15 +29,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
await authModel({
userId,
modelId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 插入记录
await PgClient.insert('modelData', {
values: data.map((item) => [

View File

@@ -3,6 +3,7 @@ import { jsonRes } from '@/service/response';
import { Chat, Model, connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -21,18 +22,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
await authModel({
modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
await connectToDatabase();
// 删除 pg 中所有该模型的数据
await PgClient.delete('modelData', {
where: [['user_id', userId], 'AND', ['model_id', modelId]]

View File

@@ -2,8 +2,7 @@ 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 { ModelSchema } from '@/types/mongoSchema';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -14,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('无权操作');
}
const { modelId } = req.query;
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
@@ -25,16 +24,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 根据 userId 获取模型信息
const model = await Model.findOne<ModelSchema>({
const { model } = await authModel({
modelId,
userId,
_id: modelId
authOwner: false
});
if (!model) {
throw new Error('模型不存在');
}
jsonRes(res, {
data: model
});

View File

@@ -4,7 +4,7 @@ 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;

View File

@@ -0,0 +1,44 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
/* 模型收藏切换 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
const collectionRecord = await Collection.findOne({
userId,
modelId
});
if (collectionRecord) {
await Collection.findByIdAndRemove(collectionRecord._id);
} else {
await Collection.create({
userId,
modelId
});
}
await Model.findByIdAndUpdate(modelId, {
'share.collection': await Collection.countDocuments({ modelId })
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,37 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { ShareModelItem } from '@/types/model';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
const userId = await authToken(req.headers.authorization);
await connectToDatabase();
// get my collections
const collections = await Collection.find({
userId
}).populate('modelId', '_id avatar name userId share');
jsonRes<ShareModelItem[]>(res, {
data: collections
.map((item: any) => ({
_id: item.modelId?._id,
avatar: item.modelId?.avatar || '/icon/logo.png',
name: item.modelId?.name || '',
userId: item.modelId?.userId || '',
share: item.modelId?.share || {},
isCollection: true
}))
.filter((item) => item.share.isShare)
});
} catch (err) {
jsonRes(res, {
data: []
});
}
}

View File

@@ -0,0 +1,60 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Collection, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { PagingData } from '@/types';
import type { ShareModelItem } from '@/types/model';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
searchText = '',
pageNum = 1,
pageSize = 20
} = req.body as { searchText: string; pageNum: number; pageSize: number };
await connectToDatabase();
const regex = new RegExp(searchText, 'i');
const where = {
$and: [
{ 'share.isShare': true },
{ $or: [{ name: { $regex: regex } }, { 'share.intro': { $regex: regex } }] }
]
};
// 获取被分享的模型
const [models, total] = await Promise.all([
Model.find(where, '_id avatar name userId share')
.sort({
'share.collection': -1
})
.limit(pageSize)
.skip((pageNum - 1) * pageSize),
Model.countDocuments(where)
]);
jsonRes<PagingData<ShareModelItem>>(res, {
data: {
pageNum,
pageSize,
data: models.map((item) => ({
_id: item._id,
avatar: item.avatar || '/icon/logo.png',
name: item.name,
userId: item.userId,
share: item.share,
isCollection: false
})),
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,11 +4,12 @@ import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { ModelUpdateParams } from '@/types/model';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, search, service, security, systemPrompt, temperature } =
const { name, avatar, search, share, service, security, systemPrompt, temperature } =
req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers;
@@ -26,6 +27,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
await authModel({
modelId,
userId
});
// 更新模型
await Model.updateOne(
{
@@ -34,8 +40,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
{
name,
avatar,
systemPrompt,
temperature,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro,
search,
security
}

View File

@@ -106,10 +106,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,

View File

@@ -202,10 +202,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
console.log('code response. time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,

View File

@@ -177,10 +177,10 @@ ${
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
step = 1;
let responseContent = '';
if (isStream) {
step = 1;
const streamResponse = await gpt35StreamResponse({
res,
stream,

View File

@@ -3,12 +3,7 @@ import { Card, Box } from '@chakra-ui/react';
import { useMarkdown } from '@/hooks/useMarkdown';
import Markdown from '@/components/Markdown';
const Empty = ({ intro }: { intro: string }) => {
const Header = ({ children }: { children: string }) => (
<Box fontSize={'lg'} fontWeight={'bold'} textAlign={'center'} pb={2}>
{children}
</Box>
);
const Empty = ({ modelName, intro }: { modelName: string; intro: string }) => {
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
@@ -24,7 +19,9 @@ const Empty = ({ intro }: { intro: string }) => {
>
{!!intro && (
<Card p={4} mb={10}>
<Header></Header>
<Box fontSize={'xl'} fontWeight={'600'} textAlign={'center'} pb={2}>
{modelName}
</Box>
<Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useMemo } from 'react';
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
import {
Box,
@@ -22,6 +22,7 @@ import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import WxConcat from '@/components/WxConcat';
import { getChatHistory, delChatHistoryById } from '@/api/chat';
import { getCollectionModels } from '@/api/model';
import { modelList } from '@/constants/model';
const SlideBar = ({
@@ -45,10 +46,30 @@ const SlideBar = ({
cacheTime: 5 * 60 * 1000
});
const { data: collectionModels = [] } = useQuery([getCollectionModels], getCollectionModels);
const models = useMemo(() => {
const myModelList = myModels.map((item) => ({
id: item._id,
name: item.name,
icon: modelList.find((model) => model.model === item?.service?.modelName)?.icon || 'model'
}));
const collectionList = collectionModels
.map((item) => ({
id: item._id,
name: item.name,
icon: 'collectionSolid' as any
}))
.filter((model) => !myModelList.find((item) => item.id === model.id));
return myModelList.concat(collectionList);
}, [collectionModels, myModels]);
const { data: chatHistory = [], mutate: loadChatHistory } = useMutation({
mutationFn: getChatHistory
});
// update history
useEffect(() => {
if (chatId && preChatId.current === '') {
loadChatHistory();
@@ -56,8 +77,11 @@ const SlideBar = ({
preChatId.current = chatId;
}, [chatId, loadChatHistory]);
// init history
useEffect(() => {
loadChatHistory();
setTimeout(() => {
loadChatHistory();
}, 1000);
}, [loadChatHistory]);
const RenderHistory = () => (
@@ -165,9 +189,9 @@ const SlideBar = ({
{isSuccess && (
<>
<Box>
{myModels.map((item) => (
{models.map((item) => (
<Flex
key={item._id}
key={item.id}
alignItems={'center'}
p={3}
borderRadius={'md'}
@@ -178,28 +202,19 @@ const SlideBar = ({
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item._id === modelId
{...(item.id === modelId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={async () => {
if (item._id === modelId) return;
resetChat(item._id);
if (item.id === modelId) return;
resetChat(item.id);
onClose();
}}
>
<MyIcon
name={
modelList.find((model) => model.model === item.service.modelName)?.icon ||
'model'
}
mr={2}
color={'white'}
w={'16px'}
h={'16px'}
/>
<MyIcon name={item.icon} mr={2} color={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>

View File

@@ -63,7 +63,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
chatId,
modelId,
name: '',
avatar: '',
avatar: '/icon/logo.png',
intro: '',
chatModel: '',
modelName: '',
history: []
@@ -155,7 +156,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
isClosable: true,
duration: 5000
});
router.replace('/model/list');
router.back();
}
setLoading(false);
return null;
@@ -466,11 +467,15 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
<Menu>
<Menu autoSelect={false}>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png"
src={
item.obj === 'Human'
? '/icon/human.png'
: chatData.avatar || '/icon/logo.png'
}
alt="avatar"
width={media(30, 20)}
height={media(30, 20)}
/>
@@ -516,7 +521,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
</Flex>
</Box>
))}
{chatData.history.length === 0 && <Empty intro={''} />}
{chatData.history.length === 0 && (
<Empty modelName={chatData.name} intro={chatData.intro} />
)}
</Box>
{/* 发送区 */}
<Box m={media('20px auto', '0 auto')} w={'100%'} maxW={media('min(750px, 100%)', 'auto')}>

View File

@@ -13,6 +13,7 @@ const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm
const Login = () => {
const router = useRouter();
const { lastRoute = '' } = router.query as { lastRoute: string };
const { isPc } = useScreen();
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo } = useUserStore();
@@ -20,9 +21,11 @@ const Login = () => {
const loginSuccess = useCallback(
(res: ResLogin) => {
setUserInfo(res.user, res.token);
router.push('/model/list');
setTimeout(() => {
router.push(lastRoute ? decodeURIComponent(lastRoute) : '/model/list');
}, 100);
},
[router, setUserInfo]
[lastRoute, router, setUserInfo]
);
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {

View File

@@ -39,7 +39,7 @@ import InputModal, { FormData as InputDataType } from './InputDataModal';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
const ModelDataCard = ({ modelId }: { modelId: string }) => {
const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean }) => {
const { Loading, setIsLoading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
@@ -133,50 +133,53 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Flex>
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
: {total}
<Box as={'span'} fontSize={'sm'}>
</Box>
</Box>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
{isOwner && (
<>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</>
)}
</Flex>
<Flex mt={4}>
{splitDataLen > 0 && <Box fontSize={'xs'}>{splitDataLen}...</Box>}
{isOwner && splitDataLen > 0 && (
<Box fontSize={'xs'}>{splitDataLen}...</Box>
)}
<Box flex={1} />
<Input
maxW={'240px'}
@@ -207,7 +210,7 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Th>{'匹配的知识点'}</Th>
<Th></Th>
<Th></Th>
<Th></Th>
{isOwner && <Th></Th>}
</Tr>
</Thead>
<Tbody>
@@ -220,33 +223,35 @@ const ModelDataCard = ({ modelId }: { modelId: string }) => {
<Box {...tdStyles.current}>{item.a || '-'}</Box>
</Td>
<Td>{ModelDataStatusMap[item.status]}</Td>
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
{isOwner && (
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
)}
</Tr>
))}
</Tbody>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import {
Box,
Card,
@@ -13,7 +13,10 @@ import {
SliderMark,
Tooltip,
Button,
Select
Select,
Grid,
Switch,
Image
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
@@ -21,14 +24,19 @@ import { UseFormReturn } from 'react-hook-form';
import { modelList, ModelVectorSearchModeMap } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { useConfirm } from '@/hooks/useConfirm';
import { useSelectFile } from '@/hooks/useSelectFile';
import { useToast } from '@/hooks/useToast';
import { fileToBase64 } from '@/utils/file';
const ModelEditForm = ({
formHooks,
canTrain,
isOwner,
handleDelModel
}: {
formHooks: UseFormReturn<ModelSchema>;
canTrain: boolean;
isOwner: boolean;
handleDelModel: () => void;
}) => {
const { openConfirm, ConfirmChild } = useConfirm({
@@ -36,12 +44,49 @@ const ModelEditForm = ({
});
const { register, setValue, getValues } = formHooks;
const [refresh, setRefresh] = useState(false);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { toast } = useToast();
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
if (file.size > 100 * 1024) {
return toast({
title: '头像需小于 100kb',
status: 'warning'
});
}
const base64 = (await fileToBase64(file)) as string;
setValue('avatar', base64);
setRefresh((state) => !state);
},
[setValue, toast]
);
return (
<>
<Card p={4}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box fontWeight={'bold'}></Box>
<Box fontWeight={'bold'}></Box>
<Flex mt={4} alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Image
src={getValues('avatar') || '/icon/logo.png'}
alt={'avatar'}
w={['28px', '36px']}
h={['28px', '36px']}
objectFit={'cover'}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={4}>
<Flex alignItems={'center'}>
@@ -49,18 +94,19 @@ const ModelEditForm = ({
:
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
modelId:
</Box>
<Box>{getValues('_id')}</Box>
</Flex>
</FormControl>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
modelId:
</Box>
<Box>{getValues('_id')}</Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
:
@@ -79,17 +125,25 @@ const ModelEditForm = ({
/1K tokens()
</Box>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}></Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Box>{getValues('share.collection')}</Box>
</Flex>
{isOwner && (
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 150px'}></Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
</Flex>
)}
</Card>
<Card p={4}>
<Box fontWeight={'bold'}></Box>
@@ -110,6 +164,7 @@ const ModelEditForm = ({
max={10}
step={1}
value={getValues('temperature')}
isDisabled={!isOwner}
onChange={(e) => {
setValue('temperature', e);
setRefresh(!refresh);
@@ -139,7 +194,10 @@ const ModelEditForm = ({
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Select {...register('search.mode', { required: '搜索模式不能为空' })}>
<Select
isDisabled={!isOwner}
{...register('search.mode', { required: '搜索模式不能为空' })}
>
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
<option key={key} value={key}>
{text}
@@ -152,17 +210,76 @@ const ModelEditForm = ({
<Box mt={4}>
<Box mb={1}></Box>
<Textarea
rows={6}
rows={8}
maxLength={-1}
{...register('systemPrompt')}
isDisabled={!isOwner}
placeholder={
canTrain
? '训练的模型会根据知识库内容,生成一部分系统提示词,因此在对话时需要消耗更多的 tokens。你可以增加提示词让效果更符合预期。例如: \n1. 请根据知识库内容回答用户问题。\n2. 知识库是电影《铃芽之旅》的内容,根据知识库内容回答。无关问题,拒绝回复!'
: '模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n注意改功能会影响对话的整体朝向'
}
{...register('systemPrompt')}
/>
</Box>
</Card>
{isOwner && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<Box fontWeight={'bold'}></Box>
<Grid gridTemplateColumns={['1fr', '1fr 410px']} gridGap={5}>
<Box>
<Flex mt={5} alignItems={'center'}>
<Box mr={3}>:</Box>
<Switch
isChecked={getValues('share.isShare')}
onChange={() => {
setValue('share.isShare', !getValues('share.isShare'));
setRefresh(!refresh);
}}
/>
<Box ml={12} mr={3}>
:
</Box>
<Switch
isChecked={getValues('share.isShareDetail')}
onChange={() => {
setValue('share.isShareDetail', !getValues('share.isShareDetail'));
setRefresh(!refresh);
}}
/>
</Flex>
<Box mt={5}>
<Box></Box>
<Textarea
mt={1}
rows={6}
maxLength={150}
{...register('share.intro')}
placeholder={'介绍模型的功能、场景等吸引更多人来使用最多150字。'}
/>
</Box>
</Box>
<Box
textAlign={'justify'}
fontSize={'sm'}
border={'1px solid #f4f4f4'}
borderRadius={'sm'}
p={3}
>
<Box fontWeight={'bold'}>Tips</Box>
<Box mt={1} as={'ul'} pl={4}>
<li>
FastGpt
使使 tokens使 tokens
</li>
<li></li>
</Box>
</Box>
</Grid>
</Card>
)}
<File onSelect={onSelectFile} />
{/* <Card p={4}>
<Box fontWeight={'bold'}>安全策略</Box>
<FormControl mt={2}>

View File

@@ -62,36 +62,38 @@ const SelectFileModal = ({
const { openConfirm, ConfirmChild } = useConfirm({
content: `确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,未完成的任务会被直接清除。一共 ${
splitRes.chunks.length
} 组,大约 ${splitRes.tokens} 个tokens, 约 ${formatPrice(
} 组,大约 ${splitRes.tokens || '数量太多,未计算'} 个tokens, 约 ${formatPrice(
splitRes.tokens * modeMap[mode].price
)}`
});
const fileText = useMemo(() => fileTextArr.join(''), [fileTextArr]);
const onSelectFile = useCallback(
async (e: File[]) => {
setSelecting(true);
try {
const fileTexts = await Promise.all(
e.map((file) => {
// @ts-ignore
const extension = file?.name?.split('.').pop().toLowerCase();
let promise = Promise.resolve();
e.map((file) => {
promise = promise.then(async () => {
const extension = file?.name?.split('.')?.pop()?.toLowerCase();
let text = '';
switch (extension) {
case 'txt':
case 'md':
return readTxtContent(file);
text = await readTxtContent(file);
break;
case 'pdf':
return readPdfContent(file);
text = await readPdfContent(file);
break;
case 'doc':
case 'docx':
return readDocContent(file);
default:
return '';
text = await readDocContent(file);
break;
}
})
);
setFileTextArr(fileTexts);
text && setFileTextArr((state) => [text].concat(state));
return;
});
});
await promise;
} catch (error: any) {
console.log(error);
toast({
@@ -131,6 +133,7 @@ const SelectFileModal = ({
const onclickImport = useCallback(() => {
const chunks = fileTextArr
.filter((item) => item)
.map((item) =>
splitText({
text: item,
@@ -138,10 +141,15 @@ const SelectFileModal = ({
})
)
.flat();
// count tokens
const tokens = chunks.map((item) =>
countChatTokens({ messages: [{ role: 'system', content: item }] })
);
let tokens: number[] = [];
// just count 100 sets of tokens
if (chunks.length < 100) {
tokens = chunks.map((item) =>
countChatTokens({ messages: [{ role: 'system', content: item }] })
);
}
setSplitRes({
tokens: tokens.reduce((sum, item) => sum + item, 0),
@@ -169,7 +177,7 @@ const SelectFileModal = ({
>
<Box mt={2} px={5} maxW={['100%', '70%']} textAlign={'justify'} color={'blackAlpha.600'}>
{fileExtension} QA
tokens
tokens{fileTextArr.length}
</Box>
{/* 拆分模式 */}
<Flex w={'100%'} px={5} alignItems={'center'} mt={4}>
@@ -200,11 +208,11 @@ const SelectFileModal = ({
)}
{/* 文本内容 */}
<Box flex={'1 0 0'} px={5} h={0} w={'100%'} overflowY={'auto'} mt={4}>
{fileTextArr.map((item, i) => (
{fileTextArr.slice(0, 100).map((item, i) => (
<Box key={i} mb={5}>
<Box mb={1}>{i + 1}</Box>
<Textarea
placeholder="文件内容"
placeholder="文件内容,空内容会自动忽略"
maxLength={-1}
rows={10}
fontSize={'xs'}
@@ -231,7 +239,11 @@ const SelectFileModal = ({
<Button variant={'outline'} colorScheme={'gray'} mr={3} onClick={onClose}>
</Button>
<Button isLoading={isLoading} isDisabled={fileText === ''} onClick={onclickImport}>
<Button
isLoading={isLoading}
isDisabled={selecting || fileTextArr[0] === ''}
onClick={onclickImport}
>
</Button>
</Flex>

View File

@@ -5,11 +5,12 @@ import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { formatModelStatus, ModelStatusEnum, modelList, defaultModel } from '@/constants/model';
import { formatModelStatus, modelList, defaultModel } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'));
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
@@ -18,6 +19,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { userInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const [model, setModel] = useState<ModelSchema>(defaultModel);
@@ -30,21 +32,24 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
return !!(openai && openai.trainName);
}, [model]);
const isOwner = useMemo(() => model.userId === userInfo?._id, [model.userId, userInfo?._id]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
setLoading(true);
try {
const res = await getModelById(modelId);
// console.log(res);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
formHooks.reset(res);
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err?.message || '获取模型异常',
status: 'error'
});
}
setLoading(false);
return null;
}, [formHooks, modelId, setLoading]);
}, [formHooks, modelId, setLoading, toast]);
useQuery([modelId], loadModel);
@@ -59,22 +64,19 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
status: 'success'
});
router.replace('/model/list');
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setLoading(false);
}, [setLoading, model, router, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
setLoading(true);
try {
router.push(`/chat?modelId=${modelId}`);
} catch (err) {
console.log('error->', err);
}
setLoading(false);
}, [setLoading, router, modelId]);
router.push(`/chat?modelId=${modelId}`);
}, [router, modelId]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
@@ -83,9 +85,11 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
try {
await putModelById(data._id, {
name: data.name,
avatar: data.avatar || '/icon/logo.png',
systemPrompt: data.systemPrompt,
temperature: data.temperature,
search: data.search,
share: data.share,
service: data.service,
security: data.security
});
@@ -93,11 +97,10 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
title: '更新成功',
status: 'success'
});
} catch (err) {
console.log('error->', err);
} catch (err: any) {
toast({
title: err as string,
status: 'success'
title: err?.message || '更新失败',
status: 'error'
});
}
setLoading(false);
@@ -151,9 +154,11 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
{isOwner && (
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
)}
</Flex>
) : (
<>
@@ -169,19 +174,26 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
{isOwner && (
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
)}
</Box>
</>
)}
</Card>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
<ModelEditForm
formHooks={formHooks}
handleDelModel={handleDelModel}
canTrain={canTrain}
isOwner={isOwner}
/>
{canTrain && !!model._id && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<ModelDataCard modelId={model._id} />
<ModelDataCard modelId={model._id} isOwner={isOwner} />
</Card>
)}
</Grid>

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { Box, Flex, Image, Button } from '@chakra-ui/react';
import type { ShareModelItem } from '@/types/model';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import styles from '../index.module.scss';
const ShareModelList = ({
models = [],
onclickCollection
}: {
models: ShareModelItem[];
onclickCollection: (modelId: string) => void;
}) => {
const router = useRouter();
return (
<>
{models.map((model) => (
<Box
key={model._id}
p={4}
border={'1px solid'}
borderColor={'gray.200'}
borderRadius={'md'}
>
<Flex alignItems={'center'}>
<Image
src={model.avatar}
alt={'avatar'}
w={['28px', '36px']}
h={['28px', '36px']}
objectFit={'cover'}
/>
<Box fontWeight={'bold'} fontSize={'lg'} ml={5}>
{model.name}
</Box>
</Flex>
<Box className={styles.intro} my={4} fontSize={'sm'} color={'blackAlpha.600'}>
{model.share.intro || '这个模型没有介绍~'}
</Box>
<Flex justifyContent={'space-between'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
color={model.isCollection ? 'blue.600' : 'alphaBlack.700'}
onClick={() => onclickCollection(model._id)}
>
<MyIcon
mr={1}
name={model.isCollection ? 'collectionSolid' : 'collectionLight'}
w={'16px'}
/>
{model.share.collection}
</Flex>
<Box>
<Button
size={'sm'}
variant={'outline'}
w={'80px'}
onClick={() => router.push(`/chat?modelId=${model._id}`)}
>
</Button>
{model.share.isShareDetail && (
<Button
ml={4}
size={'sm'}
w={'80px'}
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
>
</Button>
)}
</Box>
</Flex>
</Box>
))}
</>
);
};
export default ShareModelList;

View File

@@ -0,0 +1,7 @@
.intro {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,112 @@
import React, { useState, useRef, useCallback, useMemo } from 'react';
import { Box, Flex, Card, Grid, Input } from '@chakra-ui/react';
import { useLoading } from '@/hooks/useLoading';
import { getShareModelList, triggerModelCollection, getCollectionModels } from '@/api/model';
import { usePagination } from '@/hooks/usePagination';
import type { ShareModelItem } from '@/types/model';
import ShareModelList from './components/list';
import { useQuery } from '@tanstack/react-query';
const modelList = () => {
const { Loading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
/* 加载模型 */
const { data, isLoading, Pagination, getData, pageNum } = usePagination<ShareModelItem>({
api: getShareModelList,
pageSize: 20,
params: {
searchText
}
});
const { data: collectionModels = [], refetch: refetchCollection } = useQuery(
[getCollectionModels],
getCollectionModels
);
const models = useMemo(() => {
if (!collectionModels) return [];
return data.map((model) => ({
...model,
isCollection: !!collectionModels.find((item) => item._id === model._id)
}));
}, [collectionModels, data]);
const onclickCollection = useCallback(
async (modelId: string) => {
try {
await triggerModelCollection(modelId);
getData(pageNum);
refetchCollection();
} catch (error) {
console.log(error);
}
},
[getData, pageNum, refetchCollection]
);
return (
<>
<Card px={6} py={3}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'} fontSize={'xl'}>
</Box>
</Flex>
{collectionModels.length == 0 && (
<Box textAlign={'center'} pt={3}>
~
</Box>
)}
<Grid templateColumns={['1fr', '1fr 1fr', '1fr 1fr 1fr']} gridGap={4} mt={4}>
<ShareModelList models={collectionModels} onclickCollection={onclickCollection} />
</Grid>
</Card>
<Card mt={5} px={6} py={3}>
<Box display={['block', 'flex']} alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'} flex={1} fontSize={'xl'}>
{' '}
<Box as={'span'} fontWeight={'normal'} fontSize={'md'}>
(Beta)
</Box>
</Box>
<Box mt={[2, 0]} textAlign={'right'}>
<Input
maxW={'240px'}
size={'sm'}
value={searchText}
placeholder="搜索模型,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}
}}
/>
</Box>
</Box>
<Grid templateColumns={['1fr', '1fr 1fr', '1fr 1fr 1fr']} gridGap={4} mt={4}>
<ShareModelList models={models} onclickCollection={onclickCollection} />
</Grid>
<Box mt={4}>
<Pagination />
</Box>
</Card>
<Loading loading={isLoading} />
</>
);
};
export default modelList;

View File

@@ -0,0 +1,18 @@
import { Schema, model, models, Model as MongoModel } from 'mongoose';
import { CollectionSchema as CollectionType } from '@/types/mongoSchema';
const CollectionSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
modelId: {
type: Schema.Types.ObjectId,
ref: 'model',
required: true
}
});
export const Collection: MongoModel<CollectionType> =
models['collection'] || model('collection', CollectionSchema);

View File

@@ -14,7 +14,7 @@ const ModelSchema = new Schema({
},
avatar: {
type: String,
default: '/imgs/modelAvatar.png'
default: '/icon/logo.png'
},
systemPrompt: {
// 系统提示词
@@ -43,6 +43,26 @@ const ModelSchema = new Schema({
default: ModelVectorSearchModeEnum.hightSimilarity
}
},
share: {
isShare: {
type: Boolean,
default: false
},
isShareDetail: {
// share model detail info. false: just show name and intro
type: Boolean,
default: false
},
intro: {
type: String,
default: '',
maxlength: 150
},
collection: {
type: Number,
default: 0
}
},
service: {
chatModel: {
// 聊天时使用的模型

View File

@@ -50,3 +50,4 @@ export * from './models/pay';
export * from './models/splitData';
export * from './models/openapi';
export * from './models/promotionRecord';
export * from './models/collection';

View File

@@ -16,16 +16,34 @@ export const getOpenAIApi = (apiKey: string) => {
};
// 模型使用权校验
export const authModel = async (modelId: string, userId: string) => {
export const authModel = async ({
modelId,
userId,
authUser = true,
authOwner = true
}: {
modelId: string;
userId: string;
authUser?: boolean;
authOwner?: boolean;
}) => {
// 获取 model 数据
const model = await Model.findById<ModelSchema>(modelId);
if (!model) {
return Promise.reject('模型不存在');
}
// 凭证校验
if (userId !== String(model.userId)) {
return Promise.reject('无权使用该模型');
// 使用权限校验
if ((authOwner || (authUser && !model.share.isShare)) && userId !== String(model.userId)) {
return Promise.reject('无权操作该模型');
}
// detail 内容去除
if (!model.share.isShareDetail) {
model.systemPrompt = '';
model.temperature = 0;
}
return { model };
};
@@ -42,7 +60,7 @@ export const authChat = async ({
const userId = await authToken(authorization);
// 获取 model 数据
const { model } = await authModel(modelId, userId);
const { model } = await authModel({ modelId, userId, authOwner: false });
// 聊天内容
let content: ChatItemType[] = [];

11
src/types/model.d.ts vendored
View File

@@ -2,9 +2,11 @@ import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema } from './mongoSchema';
export interface ModelUpdateParams {
name: string;
avatar: string;
systemPrompt: string;
temperature: number;
search: ModelSchema['search'];
share: ModelSchema['share'];
service: ModelSchema['service'];
security: ModelSchema['security'];
}
@@ -17,3 +19,12 @@ export interface ModelDataItemType {
modelId: string;
userId: string;
}
export interface ShareModelItem {
_id: string;
avatar: string;
name: string;
userId: string;
share: ModelSchema['share'];
isCollection: boolean;
}

View File

@@ -41,6 +41,12 @@ export interface ModelSchema {
search: {
mode: `${ModelVectorSearchModeEnum}`;
};
share: {
isShare: boolean;
isShareDetail: boolean;
intro: string;
collection: number;
};
service: {
chatModel: `${ChatModelEnum}`; // 聊天时用的模型,训练后就是训练的模型
modelName: `${ModelNameEnum}`; // 底层模型名称,不会变
@@ -58,6 +64,11 @@ export interface ModelPopulate extends ModelSchema {
userId: UserModelSchema;
}
export interface CollectionSchema {
modelId: string;
userId: string;
}
export type ModelDataType = 0 | 1;
export interface ModelDataSchema {
_id: string;

View File

@@ -155,7 +155,7 @@ export const splitText = ({
slideLen: number;
}) => {
const textArr =
text.match(/[!?。\n.]+|[^\s]+/g)?.filter((item) => {
text.split(/(?<=[。!?\.!\?\n])/g)?.filter((item) => {
const text = item.replace(/(\\n)/g, '\n').trim();
if (text && text !== '\n') return true;
return false;
@@ -188,3 +188,12 @@ export const splitText = ({
const result = chunks.map((item) => item.arr.join(''));
return result;
};
export const fileToBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
};

View File

@@ -6,22 +6,27 @@ import { ChatModelEnum } from '@/constants/model';
const textDecoder = new TextDecoder();
const graphemer = new Graphemer();
const encMap = {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
let encMap: Record<string, Tiktoken>;
const getEncMap = () => {
if (encMap) return encMap;
encMap = {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return encMap;
};
/**
@@ -129,5 +134,5 @@ export const countChatTokens = ({
messages: { role: 'system' | 'user' | 'assistant'; content: string }[];
}) => {
const text = getChatGPTEncodingText(messages, model);
return text2TokensLen(encMap[model], text);
return text2TokensLen(getEncMap()[model], text);
};