mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
feat: model share market
This commit is contained in:
BIN
public/imgs/modelAvatar.png
Normal file
BIN
public/imgs/modelAvatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
@@ -1,7 +1,7 @@
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema';
|
||||
import { ModelUpdateParams } from '@/types/model';
|
||||
import { RequestPaging } from '../types/index';
|
||||
import { PagingData, RequestPaging } from '../types/index';
|
||||
import { Obj2Query } from '@/utils/tools';
|
||||
|
||||
/**
|
||||
@@ -93,3 +93,10 @@ 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<number>(`/model/share/getModels`, data);
|
||||
|
1
src/api/response/chat.d.ts
vendored
1
src/api/response/chat.d.ts
vendored
@@ -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[];
|
||||
|
1
src/components/Icon/icons/collectionLight.svg
Normal file
1
src/components/Icon/icons/collectionLight.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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" fill="#5D5D5D" p-id="2480"></path></svg>
|
After Width: | Height: | Size: 2.5 KiB |
1
src/components/Icon/icons/collectionSolid.svg
Normal file
1
src/components/Icon/icons/collectionSolid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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" fill="#000000" p-id="2340"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
src/components/Icon/icons/shareMarket.svg
Normal file
1
src/components/Icon/icons/shareMarket.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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 |
@@ -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;
|
||||
|
@@ -26,6 +26,12 @@ const navbarList = [
|
||||
link: '/model/list',
|
||||
activeLink: ['/model/list', '/model/detail']
|
||||
},
|
||||
{
|
||||
label: '共享',
|
||||
icon: 'shareMarket',
|
||||
link: '/model/share',
|
||||
activeLink: ['/model/share']
|
||||
},
|
||||
{
|
||||
label: '账号',
|
||||
icon: 'user',
|
||||
|
@@ -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
|
||||
|
@@ -90,6 +90,9 @@ export const theme = extendTheme({
|
||||
fonts: {
|
||||
body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||
},
|
||||
fontWeights: {
|
||||
bold: 500
|
||||
},
|
||||
breakpoints: {
|
||||
sm: '900px',
|
||||
md: '1200px',
|
||||
|
@@ -91,7 +91,7 @@ export const usePagination = <T = any,>({
|
||||
|
||||
useEffect(() => {
|
||||
mutate(1);
|
||||
}, [mutate]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
|
@@ -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[] = [];
|
||||
@@ -53,6 +53,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
|
||||
|
@@ -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,
|
||||
|
@@ -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}%')`] : [])
|
||||
];
|
||||
|
@@ -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]) => {
|
||||
|
@@ -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) => [
|
||||
|
@@ -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]]
|
||||
|
@@ -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
|
||||
});
|
||||
|
@@ -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;
|
||||
|
54
src/pages/api/model/share/getModels.ts
Normal file
54
src/pages/api/model/share/getModels.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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 { PagingData } from '@/types';
|
||||
import type { ShareModelItem } from '@/types/model';
|
||||
|
||||
/* 获取模型列表 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
// 凭证校验
|
||||
await authToken(req.headers.authorization);
|
||||
|
||||
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 = await Model.find(where, '_id avatar name userId share')
|
||||
.sort({
|
||||
'share.collection': -1
|
||||
})
|
||||
.limit(pageSize)
|
||||
.skip((pageNum - 1) * pageSize);
|
||||
|
||||
jsonRes<PagingData<ShareModelItem>>(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data: models,
|
||||
total: await Model.countDocuments(where)
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
@@ -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, 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(
|
||||
{
|
||||
@@ -36,6 +42,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
name,
|
||||
systemPrompt,
|
||||
temperature,
|
||||
'share.isShare': share.isShare,
|
||||
'share.isShareDetail': share.isShareDetail,
|
||||
'share.intro': share.intro,
|
||||
search,
|
||||
security
|
||||
}
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
@@ -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;
|
||||
@@ -469,7 +470,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
||||
<Menu>
|
||||
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
|
||||
<Image
|
||||
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
|
||||
src={item.obj === 'Human' ? '/icon/human.png' : chatData.avatar}
|
||||
alt="/icon/logo.png"
|
||||
width={media(30, 20)}
|
||||
height={media(30, 20)}
|
||||
@@ -516,7 +517,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')}>
|
||||
|
@@ -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>
|
||||
|
@@ -13,7 +13,9 @@ import {
|
||||
SliderMark,
|
||||
Tooltip,
|
||||
Button,
|
||||
Select
|
||||
Select,
|
||||
Grid,
|
||||
Switch
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
@@ -25,10 +27,12 @@ import { useConfirm } from '@/hooks/useConfirm';
|
||||
const ModelEditForm = ({
|
||||
formHooks,
|
||||
canTrain,
|
||||
isOwner,
|
||||
handleDelModel
|
||||
}: {
|
||||
formHooks: UseFormReturn<ModelSchema>;
|
||||
canTrain: boolean;
|
||||
isOwner: boolean;
|
||||
handleDelModel: () => void;
|
||||
}) => {
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
@@ -40,27 +44,26 @@ const ModelEditForm = ({
|
||||
return (
|
||||
<>
|
||||
<Card p={4}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<Box fontWeight={'bold'}>基本信息</Box>
|
||||
</Flex>
|
||||
<Box fontWeight={'bold'}>基本信息</Box>
|
||||
<FormControl mt={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
名称:
|
||||
</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 +82,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 +121,7 @@ const ModelEditForm = ({
|
||||
max={10}
|
||||
step={1}
|
||||
value={getValues('temperature')}
|
||||
isDisabled={!isOwner}
|
||||
onChange={(e) => {
|
||||
setValue('temperature', e);
|
||||
setRefresh(!refresh);
|
||||
@@ -139,7 +151,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}
|
||||
@@ -154,15 +169,73 @@ const ModelEditForm = ({
|
||||
<Textarea
|
||||
rows={6}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* <Card p={4}>
|
||||
<Box fontWeight={'bold'}>安全策略</Box>
|
||||
<FormControl mt={2}>
|
||||
|
@@ -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(
|
||||
@@ -86,6 +88,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
systemPrompt: data.systemPrompt,
|
||||
temperature: data.temperature,
|
||||
search: data.search,
|
||||
share: data.share,
|
||||
service: data.service,
|
||||
security: data.security
|
||||
});
|
||||
@@ -93,11 +96,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 +153,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 +173,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>
|
||||
|
62
src/pages/model/share/components/list.tsx
Normal file
62
src/pages/model/share/components/list.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { Dispatch, useCallback } from 'react';
|
||||
import { Card, 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 }: { models: ShareModelItem[] }) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{models.map((model) => (
|
||||
<Card key={model._id} p={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Image
|
||||
src={model.avatar}
|
||||
alt={'avatar'}
|
||||
width={'36px'}
|
||||
height={'36px'}
|
||||
objectFit={'contain'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'} ml={5}>
|
||||
{model.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box className={styles.intro} my={4} fontSize={'sm'}>
|
||||
{model.share.intro || '这个模型没有介绍~'}
|
||||
</Box>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Flex alignItems={'center'} cursor={'pointer'}>
|
||||
<MyIcon mr={1} name={'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>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareModelList;
|
7
src/pages/model/share/index.module.scss
Normal file
7
src/pages/model/share/index.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.intro {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
73
src/pages/model/share/index.tsx
Normal file
73
src/pages/model/share/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Box, Flex, Card, Grid, Input } from '@chakra-ui/react';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { getShareModelList } from '@/api/model';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import type { ShareModelItem } from '@/types/model';
|
||||
|
||||
import ShareModelList from './components/list';
|
||||
|
||||
const modelList = () => {
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const lastSearch = useRef('');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
/* 加载模型 */
|
||||
const {
|
||||
data: models,
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData
|
||||
} = usePagination<ShareModelItem>({
|
||||
api: getShareModelList,
|
||||
pageSize: 20,
|
||||
params: {
|
||||
searchText
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
{/* 头部 */}
|
||||
<Card px={6} py={3}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'} fontSize={'xl'}>
|
||||
模型共享市场
|
||||
</Box>
|
||||
<Box flex={1}>(Beta)</Box>
|
||||
<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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Grid templateColumns={['1fr', '1fr 1fr']} gridGap={4} mt={4}>
|
||||
<ShareModelList models={models} />
|
||||
</Grid>
|
||||
|
||||
<Box mt={4}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
|
||||
<Loading loading={isLoading} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default modelList;
|
@@ -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: {
|
||||
// 聊天时使用的模型
|
||||
|
@@ -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[] = [];
|
||||
|
9
src/types/model.d.ts
vendored
9
src/types/model.d.ts
vendored
@@ -5,6 +5,7 @@ export interface ModelUpdateParams {
|
||||
systemPrompt: string;
|
||||
temperature: number;
|
||||
search: ModelSchema['search'];
|
||||
share: ModelSchema['share'];
|
||||
service: ModelSchema['service'];
|
||||
security: ModelSchema['security'];
|
||||
}
|
||||
@@ -17,3 +18,11 @@ export interface ModelDataItemType {
|
||||
modelId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface ShareModelItem {
|
||||
_id: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
userId: string;
|
||||
share: ModelSchema['share'];
|
||||
}
|
||||
|
6
src/types/mongoSchema.d.ts
vendored
6
src/types/mongoSchema.d.ts
vendored
@@ -41,6 +41,12 @@ export interface ModelSchema {
|
||||
search: {
|
||||
mode: `${ModelVectorSearchModeEnum}`;
|
||||
};
|
||||
share: {
|
||||
isShare: boolean;
|
||||
isShareDetail: boolean;
|
||||
intro: string;
|
||||
collection: number;
|
||||
};
|
||||
service: {
|
||||
chatModel: `${ChatModelEnum}`; // 聊天时用的模型,训练后就是训练的模型
|
||||
modelName: `${ModelNameEnum}`; // 底层模型名称,不会变
|
||||
|
@@ -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);
|
||||
};
|
||||
|
Reference in New Issue
Block a user