This commit is contained in:
Archer
2023-10-30 13:26:42 +08:00
committed by GitHub
parent 008d0af010
commit 60ee160131
216 changed files with 4429 additions and 2229 deletions

View File

@@ -33,6 +33,7 @@ const BillTable = () => {
to: new Date()
});
const { isPc } = useSystemStore();
const [billDetail, setBillDetail] = useState<UserBillType>();
const {
data: bills,
@@ -48,8 +49,6 @@ const BillTable = () => {
}
});
const [billDetail, setBillDetail] = useState<UserBillType>();
return (
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<TableContainer px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
@@ -106,4 +105,4 @@ const BillTable = () => {
);
};
export default BillTable;
export default React.memo(BillTable);

View File

@@ -271,7 +271,7 @@ const UserInfo = () => {
>
<Avatar src={'/imgs/openai.png'} w={'18px'} />
<Box ml={2} flex={1}>
OpenAI
OpenAI/OneAPI
</Box>
<Box
w={'9px'}

View File

@@ -3,7 +3,7 @@ import { Box, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/web/support/user/api';
import { usePagination } from '@/web/common/hooks/usePagination';
import { useLoading } from '@/web/common/hooks/useLoading';
import type { informSchema } from '@/types/mongoSchema';
import type { UserInformSchema } from '@fastgpt/global/support/user/type';
import { formatTimeToChatTime } from '@/utils/tools';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@/components/Icon';
@@ -20,7 +20,7 @@ const BillTable = () => {
Pagination,
getData,
pageNum
} = usePagination<informSchema>({
} = usePagination<UserInformSchema>({
api: getInforms,
pageSize: isPc ? 20 : 10
});

View File

@@ -32,8 +32,8 @@ const OpenAIAccountModal = ({
<MyModal isOpen onClose={onClose} title={t('user.OpenAI Account Setting')}>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
OpenAI key OneAPI 线使 OpenAI
Chat Key 访
OpenAI/OneAPI 线使 OpenAI Chat
Key 访
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>

View File

@@ -12,7 +12,7 @@ import {
Box
} from '@chakra-ui/react';
import { getPayOrders, checkPayResult } from '@/web/common/bill/api';
import { PaySchema } from '@/types/mongoSchema';
import type { PaySchema } from '@fastgpt/global/support/wallet/type.d';
import dayjs from 'dayjs';
import { useQuery } from '@tanstack/react-query';
import { formatPrice } from '@fastgpt/global/common/bill/tools';

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@fastgpt/service/support/user/auth';
import { connectToDatabase, App } from '@/service/mongo';
import { FlowInputItemTypeEnum, FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { SystemInputEnum } from '@/constants/app';
const limit = 300;
@@ -46,19 +46,19 @@ async function initVariable(): Promise<any> {
const modules = jsonAPP.modules;
// 找到 variable
const variable = modules.find((item) => item.flowType === FlowModuleTypeEnum.variable);
const variable = modules.find((item) => item.flowType === FlowNodeTypeEnum.variable);
if (!variable) return await app.save();
// 找到 guide 模块
const userGuideModule = modules.find(
(item) => item.flowType === FlowModuleTypeEnum.userGuide
(item) => item.flowType === FlowNodeTypeEnum.userGuide
);
if (userGuideModule) {
userGuideModule.inputs = [
userGuideModule.inputs[0],
{
key: SystemInputEnum.variables,
type: FlowInputItemTypeEnum.systemInput,
type: FlowNodeInputTypeEnum.systemInput,
label: '对话框变量',
value: variable.inputs[0]?.value
}
@@ -66,7 +66,7 @@ async function initVariable(): Promise<any> {
} else {
modules.unshift({
moduleId: 'userGuide',
flowType: FlowModuleTypeEnum.userGuide,
flowType: FlowNodeTypeEnum.userGuide,
name: '用户引导',
position: {
x: 447.98520778293346,
@@ -75,12 +75,12 @@ async function initVariable(): Promise<any> {
inputs: [
{
key: SystemInputEnum.welcomeText,
type: FlowInputItemTypeEnum.input,
type: FlowNodeInputTypeEnum.input,
label: '开场白'
},
{
key: SystemInputEnum.variables,
type: FlowInputItemTypeEnum.systemInput,
type: FlowNodeInputTypeEnum.systemInput,
label: '对话框变量',
value: variable.inputs[0]?.value
}
@@ -90,7 +90,7 @@ async function initVariable(): Promise<any> {
}
jsonAPP.modules = jsonAPP.modules.filter(
(item) => item.flowType !== FlowModuleTypeEnum.variable
(item) => item.flowType !== FlowNodeTypeEnum.variable
);
app.modules = JSON.parse(JSON.stringify(jsonAPP.modules));

View File

@@ -4,7 +4,7 @@ import { App, connectToDatabase } from '@/service/mongo';
import { PgClient } from '@/service/pg';
import { connectionMongo } from '@fastgpt/service/common/mongo';
import { PgDatasetTableName } from '@/constants/plugin';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { delay } from '@/utils/tools';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant';
@@ -98,7 +98,7 @@ async function initMongo(limit: number) {
let success = 0;
async function initApp(limit = 100): Promise<any> {
// 遍历所有 app更新 app modules 里的 FlowModuleTypeEnum.kbSearchNode
// 遍历所有 app更新 app modules 里的 FlowNodeTypeEnum.kbSearchNode
const apps = await App.find({ inited: false }).limit(limit);
if (apps.length === 0) return;
@@ -113,7 +113,7 @@ async function initMongo(limit: number) {
modules.forEach((module) => {
// @ts-ignore
if (module.flowType === 'kbSearchNode') {
module.flowType = FlowModuleTypeEnum.datasetSearchNode;
module.flowType = FlowNodeTypeEnum.datasetSearchNode;
module.inputs.forEach((input) => {
if (input.key === 'kbList') {
input.key = 'datasets';

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, App, connectToDatabase, Collection } from '@/service/mongo';
import { Chat, App, connectToDatabase } from '@/service/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authUser } from '@fastgpt/service/support/user/auth';
import { authApp } from '@/service/utils/auth';
@@ -29,11 +29,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
appId
});
// 删除收藏列表
await Collection.deleteMany({
modelId: appId
});
// 删除分享链接
await MongoOutLink.deleteMany({
appId

View File

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

View File

@@ -1,105 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, App } from '@/service/mongo';
import type { PagingData } from '@/types';
import type { ShareAppItem } from '@/types/app';
import { authUser } from '@fastgpt/service/support/user/auth';
import { Types } from '@fastgpt/service/common/mongo';
/* 获取模型列表 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const {
searchText = '',
pageNum = 1,
pageSize = 20
} = req.body as { searchText: string; pageNum: number; pageSize: number };
const { userId } = await authUser({ req, authToken: true });
const regex = new RegExp(searchText, 'i');
const where = {
$and: [
{ 'share.isShare': true },
{
$or: [{ name: { $regex: regex } }, { intro: { $regex: regex } }]
}
]
};
const pipeline = [
{
$match: where
},
{
$lookup: {
from: 'collections',
let: { modelId: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$modelId', '$$modelId'] },
{
$eq: ['$userId', userId ? new Types.ObjectId(userId) : new Types.ObjectId()]
}
]
}
}
}
],
as: 'collections'
}
},
{
$project: {
_id: 1,
avatar: { $ifNull: ['$avatar', '/icon/logo.svg'] },
name: 1,
userId: 1,
intro: 1,
share: 1,
isCollection: {
$cond: {
if: { $gt: [{ $size: '$collections' }, 0] },
then: true,
else: false
}
}
}
},
{
$sort: { 'share.topNum': -1, 'share.collection': -1 }
},
{
$skip: (pageNum - 1) * pageSize
},
{
$limit: pageSize
}
];
// 获取被分享的模型
const [models, total] = await Promise.all([
// @ts-ignore
App.aggregate(pipeline),
App.countDocuments(where)
]);
jsonRes<PagingData<ShareAppItem>>(res, {
data: {
pageNum,
pageSize,
data: models,
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,7 +4,7 @@ import { authUser } from '@fastgpt/service/support/user/auth';
import { sseErrRes } from '@/service/response';
import { sseResponseEventEnum } from '@/constants/chat';
import { responseWrite } from '@fastgpt/service/common/response';
import { AppModuleItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { dispatchModules } from '@/pages/api/v1/chat/completions';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
@@ -13,7 +13,7 @@ import { ChatItemType } from '@/types/chat';
export type Props = {
history: ChatItemType[];
prompt: string;
modules: AppModuleItemType[];
modules: ModuleItemType[];
variables: Record<string, any>;
appId: string;
appName: string;

View File

@@ -0,0 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { createOnePlugin } from '@fastgpt/service/core/plugin/controller';
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { defaultModules } from '@fastgpt/global/core/plugin/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const body = req.body as CreateOnePluginParams;
jsonRes(res, {
data: await createOnePlugin({ userId, modules: defaultModules, ...body })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { deleteOnePlugin } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await deleteOnePlugin({ id, userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getOnePluginDetail } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getOnePluginDetail({ id, userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getUserPlugins } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getUserPlugins({ userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getPluginModuleDetail } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getPluginModuleDetail({ id, userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { getUserPlugins2Templates } from '@fastgpt/service/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
jsonRes(res, {
data: await getUserPlugins2Templates({ userId })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,23 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { updateOnePlugin } from '@fastgpt/service/core/plugin/controller';
import type { UpdatePluginParams } from '@fastgpt/global/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const body = req.body as UpdatePluginParams;
jsonRes(res, {
data: await updateOnePlugin({ userId, ...body })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -22,7 +22,7 @@ type FileType = {
/**
* Creates the multer uploader
*/
const maxSize = 50 * 1024 * 1024;
const maxSize = 500 * 1024 * 1024;
class UploadModel {
uploader = multer({
limits: {

View File

@@ -1,7 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Image } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { uploadMongoImg } from '@fastgpt/service/common/file/image/controller';
type Props = { base64Img: string };
@@ -11,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true });
const { base64Img } = req.body as Props;
const data = await uploadImg({
const data = await uploadMongoImg({
userId,
base64Img
});
@@ -24,14 +25,3 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
}
export async function uploadImg({ base64Img, userId }: Props & { userId: string }) {
const base64Data = base64Img.split(',')[1];
const { _id } = await Image.create({
userId,
binary: Buffer.from(base64Data, 'base64')
});
return `/api/system/img/${_id}`;
}

View File

@@ -1,21 +1,17 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Image } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { readMongoImg } from '@fastgpt/service/common/file/image/controller';
// get the models available to the system
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { id } = req.query;
const { id } = req.query as { id: string };
const data = await Image.findById(id);
if (!data) {
throw new Error('no image');
}
res.setHeader('Content-Type', 'image/jpeg');
res.send(data.binary);
res.send(await readMongoImg({ id }));
} catch (error) {
jsonRes(res, {
code: 500,

View File

@@ -1,14 +1,15 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@fastgpt/service/support/user/auth';
import { Pay, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { MongoPay } from '@fastgpt/service/support/wallet/pay/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
const records = await Pay.find({
const records = await MongoPay.find({
userId,
status: { $ne: 'CLOSED' }
})

View File

@@ -1,7 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
import { authUser } from '@fastgpt/service/support/user/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -14,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const { userId } = await authUser({ req, authToken: true });
const data = await Inform.countDocuments({
const data = await MongoUserInform.countDocuments({
userId,
read: false
});

View File

@@ -1,8 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -15,11 +16,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
const [informs, total] = await Promise.all([
Inform.find({ userId })
MongoUserInform.find({ userId })
.sort({ time: -1 }) // 按照创建时间倒序排列
.skip((pageNum - 1) * pageSize)
.limit(pageSize),
Inform.countDocuments({ userId })
MongoUserInform.countDocuments({ userId })
]);
jsonRes(res, {

View File

@@ -1,8 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -11,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { id } = req.query as { id: string };
await Inform.findOneAndUpdate(
await MongoUserInform.findOneAndUpdate(
{
_id: id,
userId

View File

@@ -1,11 +1,15 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Inform, connectToDatabase } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { InformTypeEnum } from '@/constants/user';
import { startSendInform } from '@/service/events/sendInform';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoUserInform } from '@fastgpt/service/support/user/inform/schema';
import { InformTypeEnum } from '@fastgpt/global/support/user/constant';
import {
sendInform2AllUser,
sendInform2OneUser
} from '@fastgpt/service/support/user/inform/controller';
export type Props = {
type: `${InformTypeEnum}`;
@@ -38,39 +42,13 @@ export async function sendInform({ type, title, content, userId }: Props) {
try {
if (userId) {
global.sendInformQueue.push(async () => {
// skip it if have same inform within 5 minutes
const inform = await Inform.findOne({
type,
title,
content,
userId,
time: { $gte: new Date(Date.now() - 5 * 60 * 1000) }
});
if (inform) return;
await Inform.create({
type,
title,
content,
userId
});
});
global.sendInformQueue.push(async () => sendInform2OneUser({ type, title, content, userId }));
startSendInform();
return;
}
// send to all user
const users = await MongoUser.find({}, '_id');
await Inform.insertMany(
users.map(({ _id }) => ({
type,
title,
content,
userId: _id
}))
);
sendInform2AllUser({ type, title, content });
} catch (error) {
console.log('send inform error', error);
}

View File

@@ -1,7 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { MongoPromotionRecord } from '@fastgpt/service/support/activity/promotion/schema';
import { authUser } from '@fastgpt/service/support/user/auth';
import mongoose from '@fastgpt/service/common/mongo';
import { MongoUser } from '@fastgpt/service/support/user/schema';
@@ -16,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
// 计算累计合
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
const countHistory: { totalAmount: number }[] = await MongoPromotionRecord.aggregate([
{
$match: {
userId: new mongoose.Types.ObjectId(userId),

View File

@@ -1,7 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@fastgpt/service/support/user/auth';
import { MongoPromotionRecord } from '@fastgpt/service/support/activity/promotion/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -13,13 +14,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { userId } = await authUser({ req, authToken: true });
const data = await promotionRecord
.find(
{
userId
},
'_id createTime type amount'
)
const data = await MongoPromotionRecord.find(
{
userId
},
'_id createTime type amount'
)
.sort({ _id: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
@@ -29,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
pageNum,
pageSize,
data,
total: await promotionRecord.countDocuments({
total: await MongoPromotionRecord.countDocuments({
userId
})
}

View File

@@ -9,12 +9,15 @@ import {
dispatchHistory,
dispatchChatInput,
dispatchChatCompletion,
dispatchKBSearch,
dispatchDatasetSearch,
dispatchAnswer,
dispatchClassifyQuestion,
dispatchContentExtract,
dispatchHttpRequest,
dispatchAppRequest
dispatchAppRequest,
dispatchRunPlugin,
dispatchPluginInput,
dispatchPluginOutput
} from '@/service/moduleDispatch';
import type { CreateChatCompletionRequest } from '@fastgpt/global/core/ai/type.d';
import type { MessageItemType } from '@/types/core/chat/type';
@@ -23,8 +26,10 @@ import { getChatHistory } from './getHistory';
import { saveChat } from '@/service/utils/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { FlowModuleTypeEnum, initModuleType } from '@/constants/flow';
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
import { initModuleType } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { RunningModuleItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
import { pushChatBill } from '@/service/common/bill/push';
import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat';
@@ -305,7 +310,7 @@ export async function dispatchModules({
detail = false
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
modules: ModuleItemType[];
user: UserModelSchema;
params?: Record<string, any>;
variables?: Record<string, any>;
@@ -425,7 +430,6 @@ export async function dispatchModules({
stream,
detail,
variables,
moduleName: module.name,
outputs: module.outputs,
user,
inputs: params
@@ -433,15 +437,18 @@ export async function dispatchModules({
const dispatchRes: Record<string, any> = await (async () => {
const callbackMap: Record<string, Function> = {
[FlowModuleTypeEnum.historyNode]: dispatchHistory,
[FlowModuleTypeEnum.questionInput]: dispatchChatInput,
[FlowModuleTypeEnum.answerNode]: dispatchAnswer,
[FlowModuleTypeEnum.chatNode]: dispatchChatCompletion,
[FlowModuleTypeEnum.datasetSearchNode]: dispatchKBSearch,
[FlowModuleTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowModuleTypeEnum.contentExtract]: dispatchContentExtract,
[FlowModuleTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowModuleTypeEnum.app]: dispatchAppRequest
[FlowNodeTypeEnum.historyNode]: dispatchHistory,
[FlowNodeTypeEnum.questionInput]: dispatchChatInput,
[FlowNodeTypeEnum.answerNode]: dispatchAnswer,
[FlowNodeTypeEnum.chatNode]: dispatchChatCompletion,
[FlowNodeTypeEnum.datasetSearchNode]: dispatchDatasetSearch,
[FlowNodeTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowNodeTypeEnum.contentExtract]: dispatchContentExtract,
[FlowNodeTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowNodeTypeEnum.runApp]: dispatchAppRequest,
[FlowNodeTypeEnum.pluginModule]: dispatchRunPlugin,
[FlowNodeTypeEnum.pluginInput]: dispatchPluginInput,
[FlowNodeTypeEnum.pluginOutput]: dispatchPluginOutput
};
if (callbackMap[module.flowType]) {
return callbackMap[module.flowType](props);
@@ -449,9 +456,21 @@ export async function dispatchModules({
return {};
})();
const formatResponseData = (() => {
if (!dispatchRes[TaskResponseKeyEnum.responseData]) return undefined;
if (Array.isArray(dispatchRes[TaskResponseKeyEnum.responseData]))
return dispatchRes[TaskResponseKeyEnum.responseData];
return {
...dispatchRes[TaskResponseKeyEnum.responseData],
moduleName: module.name,
moduleType: module.flowType
};
})();
return moduleOutput(module, {
[SystemOutputEnum.finish]: true,
...dispatchRes
...dispatchRes,
[TaskResponseKeyEnum.responseData]: formatResponseData
});
}
@@ -468,7 +487,7 @@ export async function dispatchModules({
/* init store modules to running modules */
function loadModules(
modules: AppModuleItemType[],
modules: ModuleItemType[],
variables: Record<string, any>
): RunningModuleItemType[] {
return modules.map((module) => {
@@ -495,12 +514,19 @@ function loadModules(
value: replacedVal
};
}),
outputs: module.outputs.map((item) => ({
key: item.key,
answer: item.key === TaskResponseKeyEnum.answerText,
value: undefined,
targets: item.targets
}))
outputs: module.outputs
.map((item) => ({
key: item.key,
answer: item.key === TaskResponseKeyEnum.answerText,
value: undefined,
targets: item.targets
}))
.sort((a, b) => {
// finish output always at last
if (a.key === SystemOutputEnum.finish) return 1;
if (b.key === SystemOutputEnum.finish) return -1;
return 0;
})
};
});
}

View File

@@ -1,237 +0,0 @@
import React, { useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { EditFormType } from '@/web/core/app/basicSettings';
import { useForm } from 'react-hook-form';
import {
Box,
BoxProps,
Button,
Flex,
Link,
ModalBody,
ModalFooter,
Switch,
Textarea
} from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { Prompt_QuotePromptList, Prompt_QuoteTemplateList } from '@/global/core/prompt/AIChat';
import { chatModelList, feConfigs } from '@/web/common/system/staticData';
import MySlider from '@/components/Slider';
import { SystemInputEnum } from '@/constants/app';
import dynamic from 'next/dynamic';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type.d';
const PromptTemplate = dynamic(() => import('@/components/PromptTemplate'));
const AIChatSettingsModal = ({
isAdEdit,
onClose,
onSuccess,
defaultData
}: {
isAdEdit?: boolean;
onClose: () => void;
onSuccess: (e: EditFormType['chatModel']) => void;
defaultData: EditFormType['chatModel'];
}) => {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { register, handleSubmit, getValues, setValue } = useForm({
defaultValues: defaultData
});
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
key: 'quoteTemplate' | 'quotePrompt';
templates: PromptTemplateItem[];
}>();
const tokenLimit = useMemo(() => {
return chatModelList.find((item) => item.model === getValues('model'))?.maxToken || 4000;
}, [getValues, refresh]);
const LabelStyles: BoxProps = {
fontSize: ['sm', 'md']
};
const selectTemplateBtn: BoxProps = {
color: 'myBlue.600',
cursor: 'pointer'
};
return (
<MyModal
isOpen
title={
<Flex alignItems={'flex-end'}>
{t('app.AI Advanced Settings')}
{feConfigs?.show_doc && (
<Link
href={`${feConfigs.docUrl}/docs/use-cases/ai_settings/`}
target={'_blank'}
ml={1}
textDecoration={'underline'}
fontWeight={'normal'}
fontSize={'md'}
>
</Link>
)}
</Flex>
}
isCentered
w={'700px'}
h={['90vh', 'auto']}
>
<ModalBody flex={['1 0 0', 'auto']} overflowY={'auto'}>
{isAdEdit && (
<Flex alignItems={'center'}>
<Box {...LabelStyles} w={'80px'}>
AI内容
</Box>
<Box flex={1} ml={'10px'}>
<Switch
isChecked={getValues(SystemInputEnum.isResponseAnswerText)}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
setValue(SystemInputEnum.isResponseAnswerText, value);
setRefresh((state) => !state);
}}
/>
</Box>
</Flex>
)}
<Flex alignItems={'center'} mb={10} mt={isAdEdit ? 8 : 5}>
<Box {...LabelStyles} mr={2} w={'80px'}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '严谨', value: 0 },
{ label: '发散', value: 10 }
]}
width={'95%'}
min={0}
max={10}
value={getValues('temperature')}
onChange={(e) => {
setValue('temperature', e);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={12} mb={10}>
<Box {...LabelStyles} mr={2} w={'80px'}>
</Box>
<Box flex={1} ml={'10px'}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: `${tokenLimit}`, value: tokenLimit }
]}
width={'95%'}
min={100}
max={tokenLimit}
step={50}
value={getValues('maxToken')}
onChange={(val) => {
setValue('maxToken', val);
setRefresh(!refresh);
}}
/>
</Box>
</Flex>
<Box>
<Flex {...LabelStyles} mb={1}>
<MyTooltip
label={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择引用内容模板',
key: 'quoteTemplate',
templates: Prompt_QuoteTemplateList
})
}
>
</Box>
</Flex>
<Textarea
rows={6}
placeholder={
t('template.Quote Content Tip', { default: Prompt_QuoteTemplateList[0].value }) || ''
}
borderColor={'myGray.100'}
{...register('quoteTemplate')}
/>
</Box>
<Box mt={4}>
<Flex {...LabelStyles} mb={1}>
<MyTooltip
label={t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value })}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
onClick={() =>
setSelectTemplateData({
title: '选择引用提示词模板',
key: 'quotePrompt',
templates: Prompt_QuotePromptList
})
}
>
</Box>
</Flex>
<Textarea
rows={11}
placeholder={
t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value }) || ''
}
borderColor={'myGray.100'}
{...register('quotePrompt')}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'base'} onClick={onClose}>
{t('Cancel')}
</Button>
<Button ml={4} onClick={handleSubmit(onSuccess)}>
{t('Confirm')}
</Button>
</ModalFooter>
{!!selectTemplateData && (
<PromptTemplate
title={selectTemplateData.title}
templates={selectTemplateData.templates}
onClose={() => setSelectTemplateData(undefined)}
onSuccess={(e) => setValue(selectTemplateData.key, e)}
/>
)}
</MyModal>
);
};
export default AIChatSettingsModal;

View File

@@ -1,36 +1,36 @@
import React, { useCallback, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { FlowInputItemTypeEnum } from '@/constants/flow';
import { FlowOutputTargetItemType } from '@/types/core/app/flow';
import { AppModuleItemType } from '@/types/app';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeOutputTargetItemType } from '@fastgpt/global/core/module/node/type';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import type { AppSchema } from '@/types/mongoSchema';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { AppTypeEnum, SystemOutputEnum } from '@/constants/app';
import { AppTypeEnum } from '@/constants/app';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import ChatTest, { type ChatTestComponentRef } from './ChatTest';
import { useFlowStore } from './Provider';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/module/Flow/ChatTest';
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
const ImportSettings = dynamic(() => import('./ImportSettings'));
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
type Props = { app: AppSchema; onCloseSettings: () => void };
type Props = { app: AppSchema; onClose: () => void };
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
app,
ChatTestRef,
testModules,
setTestModules,
onCloseSettings
onClose
}: Props & {
ChatTestRef: React.RefObject<ChatTestComponentRef>;
testModules?: AppModuleItemType[];
setTestModules: React.Dispatch<AppModuleItemType[] | undefined>;
testModules?: ModuleItemType[];
setTestModules: React.Dispatch<ModuleItemType[] | undefined>;
}) {
const theme = useTheme();
const { t } = useTranslation();
@@ -38,54 +38,11 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { updateAppDetail } = useUserStore();
const { nodes, edges, onFixView } = useFlowStore();
const flow2AppModules = useCallback(() => {
const modules: AppModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((item) => ({
...item,
connected: item.connected ?? item.type !== FlowInputItemTypeEnum.target
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected =
input.connected ||
!!edges.find(
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter(
(edge) =>
edge.source === module.moduleId &&
edge.sourceHandle === output.key &&
edge.targetHandle
)
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
}, [edges, nodes]);
const { nodes, edges, onFixView } = useFlowProviderStore();
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: () => {
const modules = flow2AppModules();
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
@@ -127,7 +84,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
variant={'base'}
aria-label={''}
onClick={() => {
onCloseSettings();
onClose();
onFixView();
}}
/>
@@ -155,7 +112,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
aria-label={'save'}
onClick={() =>
copyData(
JSON.stringify(flow2AppModules(), null, 2),
JSON.stringify(flowNode2Modules({ nodes, edges }), null, 2),
t('app.Export Config Successful')
)
}
@@ -181,7 +138,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
aria-label={'save'}
variant={'base'}
onClick={() => {
setTestModules(flow2AppModules());
setTestModules(flowNode2Modules({ nodes, edges }));
}}
/>
</MyTooltip>
@@ -206,7 +163,7 @@ const Header = (props: Props) => {
const { app } = props;
const ChatTestRef = useRef<ChatTestComponentRef>(null);
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
const [testModules, setTestModules] = useState<ModuleItemType[]>();
return (
<>

View File

@@ -1,137 +0,0 @@
import { AppModuleItemType } from '@/types/app';
import { AppSchema } from '@/types/mongoSchema';
import React, {
useMemo,
useCallback,
useRef,
forwardRef,
useImperativeHandle,
ForwardedRef
} from 'react';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { getGuideModule } from '@/global/core/app/modules/utils';
export type ChatTestComponentRef = {
resetChatTest: () => void;
};
const ChatTest = (
{
app,
modules = [],
onClose
}: {
app: AppSchema;
modules?: AppModuleItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
) => {
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const isOpen = useMemo(() => modules && modules.length > 0, [modules]);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
url: '/api/chat/chatTest',
data: {
history,
prompt: chatList[chatList.length - 2].value,
modules,
variables,
appId: app._id,
appName: `调试-${app.name}`
},
onMessage: generatingMessage,
abortSignal: controller
});
return { responseText, responseData };
},
[app._id, app.name, modules]
);
useImperativeHandle(ref, () => ({
resetChatTest() {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}
}));
return (
<>
<Flex
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? ['100%', '460px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
<Flex py={4} px={5} whiteSpace={'nowrap'}>
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
</Box>
<MyTooltip label={'重置'}>
<IconButton
className="chat"
size={'sm'}
icon={<MyIcon name={'clear'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appAvatar={app.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(modules)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
</Flex>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
</>
);
};
export default React.memo(forwardRef(ChatTest));

View File

@@ -1,53 +0,0 @@
import React, { useState } from 'react';
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowStore } from './Provider';
const ImportSettings = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [value, setValue] = useState('');
const { setNodes, setEdges, initData } = useFlowStore();
return (
<MyModal isOpen w={'600px'} onClose={onClose} title={t('app.Import Config')}>
<ModalBody>
<Textarea
placeholder={t('app.Paste Config') || 'app.Paste Config'}
defaultValue={value}
rows={16}
onChange={(e) => setValue(e.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button
variant="base"
onClick={() => {
if (!value) {
return onClose();
}
try {
const data = JSON.parse(value);
setEdges([]);
setNodes([]);
setTimeout(() => {
initData(data);
}, 10);
onClose();
} catch (error) {
toast({
title: t('app.Import Config Failed')
});
}
}}
>
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ImportSettings);

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeAPP);

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeAnswer);

View File

@@ -1,148 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import { Box, Input, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
import MyIcon from '@/components/Icon';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
import { useTranslation } from 'react-i18next';
import SourceHandle from '../render/SourceHandle';
import MyTooltip from '@/components/MyTooltip';
import { onChangeNode } from '../Provider';
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={{
[SpecialInputKeyEnum.agents]: ({
key: agentKey,
value: agents = [],
...props
}: {
key: string;
value?: ClassifyQuestionAgentItemType[];
}) => (
<Box>
{agents.map((item, i) => (
<Box key={item.key} mb={4}>
<Flex alignItems={'center'}>
<MyTooltip label={t('common.Delete')}>
<MyIcon
mt={1}
mr={2}
name={'minus'}
w={'14px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
const newInputValue = agents.filter((input) => input.key !== item.key);
const newOutputVal = outputs.filter(
(output) => output.key !== item.key
);
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: {
...props,
key: agentKey,
value: newInputValue
}
});
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputVal
});
}}
/>
</MyTooltip>
<Box flex={1}>{i + 1}</Box>
</Flex>
<Box position={'relative'}>
<Textarea
rows={2}
mt={1}
defaultValue={item.value}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: {
...props,
key: agentKey,
value: newVal
}
});
}}
/>
<SourceHandle handleKey={item.key} valueType={FlowValueTypeEnum.boolean} />
</Box>
</Box>
))}
<Button
onClick={() => {
const key = nanoid();
const newInputValue = agents.concat({ value: '', key });
const newOutputValue = outputs.concat({
key,
label: '',
type: FlowOutputItemTypeEnum.hidden,
targets: []
});
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: {
...props,
key: agentKey,
value: newInputValue
}
});
onChangeNode({
moduleId,
type: 'outputs',
key: agentKey,
value: newOutputValue
});
}}
>
</Button>
</Box>
)
}}
/>
</Container>
</NodeCard>
);
};
export default React.memo(NodeCQNode);

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeChat = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeChat);

View File

@@ -1,26 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@/types/core/app/flow';
import NodeCard from '../modules/NodeCard';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeDatasetSearch = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeDatasetSearch);

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {
return <NodeCard {...data}></NodeCard>;
};
export default React.memo(NodeAnswer);

View File

@@ -1,222 +0,0 @@
import React, { useState } from 'react';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@/types/core/app/flow';
import { useTranslation } from 'next-i18next';
import NodeCard from '../modules/NodeCard';
import Container from '../modules/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import Divider from '../modules/Divider';
import { ContextExtractAgentItemType } from '@/types/app';
import RenderOutput from '../render/RenderOutput';
import MyIcon from '@/components/Icon';
import ExtractFieldModal from '../modules/ExtractFieldModal';
import { ContextExtractEnum } from '@/constants/flow/flowField';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { useFlowStore, onChangeNode } from '../Provider';
const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId } = data;
const { t } = useTranslation();
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const { onDelEdge } = useFlowStore();
return (
<NodeCard minW={'400px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={{
[ContextExtractEnum.extractKeys]: ({
value: extractKeys = [],
...props
}: {
value?: ContextExtractAgentItemType[];
}) => (
<Box pt={2}>
<Box position={'absolute'} top={0} right={0}>
<Button
variant={'base'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() =>
setEditExtractField({
desc: '',
key: '',
required: true
})
}
>
</Button>
</Box>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th> key</Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
const newInputValue = extractKeys.filter(
(extract) => item.key !== extract.key
);
const newOutputVal = outputs.filter(
(output) => output.key !== item.key
);
onChangeNode({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: {
...props,
value: newInputValue
}
});
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputVal
});
onDelEdge({ moduleId, sourceHandle: item.key });
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)
}}
/>
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
{!!editExtractFiled && (
<ExtractFieldModal
defaultField={editExtractFiled}
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === ContextExtractEnum.extractKeys)?.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
const newInputs = exists
? extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
: extracts.concat(data);
onChangeNode({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: {
...inputs.find((input) => input.key === ContextExtractEnum.extractKeys),
value: newInputs
}
});
if (!exists) {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.concat({
key: data.key,
label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
targets: []
})
});
} else {
if (editExtractFiled.key === data.key) {
// update
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.map((output) =>
output.key === data.key
? {
...output,
label: `提取结果-${data.desc}`
}
: output
)
});
} else {
// del and push
const newOutputs = outputs.filter((output) => output.key !== editExtractFiled.key);
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputs
});
setTimeout(() => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: newOutputs.concat({
key: data.key,
label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
targets: []
})
});
}, 10);
}
}
setEditExtractField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodeExtract);

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeHistory = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId } = data;
return (
<NodeCard minW={'300px'} {...data}>
<Divider text="Input" />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeHistory);

View File

@@ -1,80 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import { Box, Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import RenderOutput from '../render/RenderOutput';
import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import { onChangeNode } from '../Provider';
const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
<Button
variant={'base'}
mt={5}
leftIcon={<SmallAddIcon />}
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'addInput',
key,
value: {
key,
valueType: FlowValueTypeEnum.string,
type: FlowInputItemTypeEnum.target,
label: `入参${inputs.length - 1}`,
edit: true
}
});
}}
>
</Button>
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
<Box textAlign={'right'} mt={5}>
<Button
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'outputs',
key,
value: [
{
key,
label: `出参${outputs.length}`,
valueType: FlowValueTypeEnum.string,
type: FlowOutputItemTypeEnum.source,
edit: true,
targets: []
}
].concat(outputs as any)
});
}}
>
</Button>
</Box>
</Container>
</NodeCard>
);
};
export default React.memo(NodeHttp);

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Container from '../modules/Container';
import RenderOutput from '../render/RenderOutput';
const QuestionInputNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'240px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(QuestionInputNode);

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { Flex, Box } from '@chakra-ui/react';
import NodeCard from '../modules/NodeCard';
import { SystemInputEnum } from '@/constants/app';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import Label from '../modules/Label';
const NodeTFSwitch = ({ data }: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard minW={'220px'} {...data}>
<Divider text="输入输出" />
<Container h={'100px'} py={0} px={0} display={'flex'} alignItems={'center'}>
<Box flex={1} pl={'12px'}>
<Label
required
description="接收到 false、0、null、undefined或空字符串时执行 False反之执行 True"
>
</Label>
<Handle
style={{
top: '50%',
left: '0',
transform: 'translate(-50%,-50%)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
id={SystemInputEnum.switch}
type="target"
position={Position.Left}
onConnect={(params) => console.log('input onConnect', params)}
/>
</Box>
<Box flex={1} pr={'12px'}>
<Flex alignItems={'center'} justifyContent={'flex-end'} mb={'26px'} position={'relative'}>
<Label>True</Label>
<Handle
style={{
top: '0',
right: '-12px',
transform: 'translate(50%,5px)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
id={'true'}
type="source"
position={Position.Right}
onConnect={(params) => console.log('handle onConnect', params)}
/>
</Flex>
<Flex alignItems={'center'} justifyContent={'flex-end'} position={'relative'}>
<Label>False</Label>
<Handle
style={{
bottom: '0',
right: '-12px',
transform: 'translate(50%,-5px)',
width: '12px',
height: '12px',
background: '#9CA2A8'
}}
id={'false'}
type="source"
position={Position.Right}
onConnect={(params) => console.log('handle onConnect', params)}
/>
</Flex>
</Box>
</Container>
</NodeCard>
);
};
export default React.memo(NodeTFSwitch);

View File

@@ -1,243 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import {
Box,
Flex,
Textarea,
useTheme,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Switch
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowModuleItemType } from '@/types/core/app/flow';
import { SystemInputEnum } from '@/constants/app';
import { welcomeTextTip, variableTip, questionGuideTip } from '@/constants/flow/ModuleTemplate';
import { onChangeNode } from '../Provider';
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import Container from '../modules/Container';
import NodeCard from '../modules/NodeCard';
import { VariableItemType } from '@/types/app';
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
const theme = useTheme();
return (
<>
<NodeCard minW={'300px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<WelcomeText data={data} />
<Box pt={4} pb={2}>
<ChatStartVariable data={data} />
</Box>
<Box pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} />
</Box>
</Container>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
export function WelcomeText({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const welcomeText = useMemo(
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
[inputs]
);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
<Box></Box>
<MyTooltip label={welcomeTextTip} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={welcomeTextTip}
onChange={(e) => {
onChangeNode({
moduleId,
key: SystemInputEnum.welcomeText,
type: 'inputs',
value: {
...welcomeText,
value: e.target.value
}
});
}}
/>
)}
</>
);
}
function ChatStartVariable({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const [editVariable, setEditVariable] = useState<VariableItemType>();
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId,
key: SystemInputEnum.variables,
type: 'inputs',
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[inputs, onChangeNode, moduleId]
);
const onclickSubmit = useCallback(
({ variable }: { variable: VariableItemType }) => {
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
setEditVariable(undefined);
},
[updateVariables, variables]
);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'variable'} mr={2} w={'16px'} color={'#fb7c3d'} />
<Box></Box>
<MyTooltip label={variableTip} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Flex
ml={2}
textAlign={'right'}
cursor={'pointer'}
px={3}
py={'2px'}
borderRadius={'md'}
_hover={{ bg: 'myGray.200' }}
onClick={() => {
const newVariable = addVariable();
updateVariables(variables.concat(newVariable));
setEditVariable(newVariable);
}}
>
+&ensp;
</Flex>
</Flex>
{variables.length > 0 && (
<TableContainer borderWidth={'1px'} borderBottom="none" borderRadius={'lg'}>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th> key</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item, index) => (
<Tr key={index}>
<Td>{item.label} </Td>
<Td>{item.key}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditVariable(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
updateVariables(variables.filter((variable) => variable.id !== item.id))
}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
{!!editVariable && (
<VariableEditModal
defaultVariable={editVariable}
onClose={() => setEditVariable(undefined)}
onSubmit={onclickSubmit}
/>
)}
</>
);
}
function QuestionGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
return (
<Flex alignItems={'center'}>
<MyIcon name={'questionGuide'} mr={2} w={'16px'} />
<Box></Box>
<MyTooltip label={questionGuideTip} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Switch
isChecked={questionGuide}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
onChangeNode({
moduleId,
key: SystemInputEnum.questionGuide,
type: 'inputs',
value: {
...inputs.find((item) => item.key === SystemInputEnum.questionGuide),
value
}
});
}}
/>
</Flex>
);
}

View File

@@ -1,131 +0,0 @@
/* Abandon */
import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Container from '../modules/Container';
import { SystemInputEnum, VariableInputEnum } from '@/constants/app';
import type { VariableItemType } from '@/types/app';
import MyIcon from '@/components/Icon';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import VariableEditModal, { addVariable } from '../../../VariableEditModal';
import { onChangeNode } from '../Provider';
export const defaultVariable: VariableItemType = {
id: nanoid(),
key: 'key',
label: 'label',
type: VariableInputEnum.input,
required: true,
maxLen: 50,
enums: [{ value: '' }]
};
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, moduleId } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === SystemInputEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const [editVariable, setEditVariable] = useState<VariableItemType>();
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId,
key: SystemInputEnum.variables,
type: 'inputs',
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[inputs, onChangeNode, moduleId]
);
const onclickSubmit = useCallback(
({ variable }: { variable: VariableItemType }) => {
updateVariables(variables.map((item) => (item.id === variable.id ? variable : item)));
setEditVariable(undefined);
},
[updateVariables, variables]
);
return (
<>
<NodeCard minW={'300px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th> key</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item, index) => (
<Tr key={index}>
<Td>{item.label} </Td>
<Td>{item.key}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditVariable(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
updateVariables(variables.filter((variable) => variable.id !== item.id))
}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Box mt={2} textAlign={'right'}>
<Button
variant={'base'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => {
const newVariable = addVariable();
updateVariables(variables.concat(newVariable));
setEditVariable(newVariable);
}}
>
</Button>
</Box>
</Container>
</NodeCard>
{!!editVariable && (
<VariableEditModal
defaultVariable={editVariable}
onClose={() => setEditVariable(undefined)}
onSubmit={onclickSubmit}
/>
)}
</>
);
};
export default React.memo(NodeUserGuide);

View File

@@ -1,342 +0,0 @@
import {
type Node,
type NodeChange,
type Edge,
type EdgeChange,
useNodesState,
useEdgesState,
Connection,
addEdge
} from 'reactflow';
import type {
FlowModuleItemType,
FlowOutputTargetItemType,
FlowModuleItemChangeProps
} from '@/types/core/app/flow';
import React, {
type SetStateAction,
type Dispatch,
useContext,
useCallback,
createContext,
useRef,
useEffect
} from 'react';
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@/web/common/hooks/useToast';
import { FlowModuleTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { useTranslation } from 'next-i18next';
import { AppModuleItemType } from '@/types/app';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type useFlowStoreType = {
appId: string;
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
nodes: Node<FlowModuleItemType, string | undefined>[];
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onDelNode: (nodeId: string) => void;
onChangeNode: (e: FlowModuleItemChangeProps) => void;
onCopyNode: (nodeId: string) => void;
onDelEdge: (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
onDelConnect: (id: string) => void;
onConnect: ({ connect }: { connect: Connection }) => any;
initData: (modules: AppModuleItemType[]) => void;
};
const StateContext = createContext<useFlowStoreType>({
appId: '',
reactFlowWrapper: null,
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
): void {
return;
},
onNodesChange: function (changes: NodeChange[]): void {
return;
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
return;
},
onEdgesChange: function (changes: EdgeChange[]): void {
return;
},
onFixView: function (): void {
return;
},
onDelNode: function (nodeId: string): void {
return;
},
onChangeNode: function (e: FlowModuleItemChangeProps): void {
return;
},
onCopyNode: function (nodeId: string): void {
return;
},
onDelEdge: function (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
return;
},
onDelConnect: function (id: string): void {
return;
},
onConnect: function ({ connect }: { connect: Connection }) {
return;
},
initData: function (modules: AppModuleItemType[]): void {
throw new Error('Function not implemented.');
}
});
export const useFlowStore = () => useContext(StateContext);
export const FlowProvider = ({ appId, children }: { appId: string; children: React.ReactNode }) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onFixView = useCallback(() => {
const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
}, []);
const onDelEdge = useCallback(
({
moduleId,
sourceHandle,
targetHandle
}: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => {
if (!sourceHandle && !targetHandle) return;
setEdges((state) =>
state.filter((edge) => {
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
return true;
})
);
},
[setEdges]
);
const onDelConnect = useCallback(
(id: string) => {
setEdges((state) => state.filter((item) => item.id !== id));
},
[setEdges]
);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
if (source?.flowType === FlowModuleTypeEnum.classifyQuestion) {
return FlowValueTypeEnum.boolean;
}
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
})();
const targetType = nodes
.find((node) => node.id === connect.target)
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
if (!sourceType || !targetType) {
return toast({
status: 'warning',
title: t('app.Connection is invalid')
});
}
if (
sourceType !== FlowValueTypeEnum.any &&
targetType !== FlowValueTypeEnum.any &&
sourceType !== targetType
) {
return toast({
status: 'warning',
title: t('app.Connection type is different')
});
}
setEdges((state) =>
addEdge(
{
...connect,
type: 'buttonedge',
animated: true,
data: {
onDelete: onDelConnect
}
},
state
)
);
},
[nodes, onDelConnect, setEdges, t, toast]
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.id !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
},
[setEdges, setNodes]
);
const onChangeNode = useCallback(
({ moduleId, key, type = 'inputs', value }: FlowModuleItemChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
const updateObj: Record<string, any> = {};
if (type === 'inputs') {
updateObj.inputs = node.data.inputs.map((item) => (item.key === key ? value : item));
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.inputs = node.data.inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
}
} else if (type === 'delInput') {
onDelEdge({ moduleId, targetHandle: key });
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
} else if (type === 'attr') {
updateObj[key] = value;
} else if (type === 'outputs') {
// del output connect
const delOutputs = node.data.outputs.filter(
(item) => !value.find((output: FlowOutputTargetItemType) => output.key === item.key)
);
delOutputs.forEach((output) => {
onDelEdge({ moduleId, sourceHandle: output.key });
});
updateObj.outputs = value;
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
},
[onDelEdge, setNodes, toast]
);
const onCopyNode = useCallback(
(nodeId: string) => {
setNodes((nodes) => {
const node = nodes.find((node) => node.id === nodeId);
if (!node) return nodes;
const template = {
logo: node.data.logo,
name: node.data.name,
intro: node.data.intro,
description: node.data.description,
flowType: node.data.flowType,
inputs: node.data.inputs,
outputs: node.data.outputs,
showStatus: node.data.showStatus
};
return nodes.concat(
appModule2FlowNode({
item: {
...template,
moduleId: nanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 }
}
})
);
});
},
[setNodes]
);
const initData = useCallback(
(modules: AppModuleItemType[]) => {
const edges = appModule2FlowEdge({
modules,
onDelete: onDelConnect
});
setEdges(edges);
setNodes(modules.map((item) => appModule2FlowNode({ item })));
onFixView();
},
[onDelConnect, setEdges, setNodes, onFixView]
);
// use eventbus to avoid refresh ReactComponents
useEffect(() => {
const update = (e: FlowModuleItemChangeProps) => {
onChangeNode(e);
};
eventBus.on(EventNameEnum.updaterNode, update);
return () => {
eventBus.off(EventNameEnum.updaterNode);
};
}, [onChangeNode]);
const value = {
appId,
reactFlowWrapper,
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
onFixView,
onDelNode,
onChangeNode,
onCopyNode,
onDelEdge,
onDelConnect,
onConnect,
initData
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowModuleItemChangeProps) => {
eventBus.emit(EventNameEnum.updaterNode, e);
};

View File

@@ -1,144 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
import { FlowModuleTemplateType } from '@/types/core/app/flow';
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { useFlowStore } from './Provider';
import { customAlphabet } from 'nanoid';
import { appModule2FlowNode } from '@/utils/adapt';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
const ModuleTemplateList = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => {
const { nodes, setNodes, reactFlowWrapper } = useFlowStore();
const { isPc } = useSystemStore();
const { x, y, zoom } = useViewport();
const filterTemplates = useMemo(() => {
const guideModulesIndex = ModuleTemplates.findIndex((item) => item.label === '引导模块');
const guideModule: {
label: string;
list: FlowModuleTemplateType[];
} = JSON.parse(JSON.stringify(ModuleTemplates[guideModulesIndex]));
if (nodes?.find((item) => item.type === FlowModuleTypeEnum.userGuide)) {
const index = guideModule.list.findIndex(
(item) => item.flowType === FlowModuleTypeEnum.userGuide
);
guideModule.list.splice(index, 1);
}
if (nodes?.find((item) => item.type === FlowModuleTypeEnum.variable)) {
const index = guideModule.list.findIndex(
(item) => item.flowType === FlowModuleTypeEnum.variable
);
guideModule.list.splice(index, 1);
}
return [
...ModuleTemplates.slice(0, guideModulesIndex),
guideModule,
...ModuleTemplates.slice(guideModulesIndex + 1)
];
}, [nodes]);
const onAddNode = useCallback(
({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
if (!reactFlowWrapper?.current) return;
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
setNodes((state) =>
state.concat(
appModule2FlowNode({
item: {
...template,
moduleId: nanoid(),
position: { x: mouseX, y: mouseY }
}
})
)
);
},
[reactFlowWrapper, setNodes, x, y, zoom]
);
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={'360px'}
onClick={onClose}
/>
<Flex
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'65px'}
left={0}
pb={4}
h={isOpen ? 'calc(100% - 100px)' : '0'}
w={isOpen ? ['100%', '360px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'20px'}
overflow={'hidden'}
transition={'.2s ease'}
userSelect={'none'}
>
<Box w={['100%', '330px']} py={4} px={5} fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Box flex={'1 0 0'} overflow={'overlay'}>
<Box w={['100%', '330px']} mx={'auto'}>
{filterTemplates.map((item) =>
item.list.map((item) => (
<Flex
key={item.name}
alignItems={'center'}
p={5}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'md'}
draggable
onDragEnd={(e) => {
if (e.clientX < 360) return;
onAddNode({
template: item,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (isPc) return;
onClose();
onAddNode({
template: item,
position: { x: e.clientX, y: e.clientY }
});
}}
>
<Avatar src={item.logo} w={'34px'} objectFit={'contain'} borderRadius={'0'} />
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{item.name}</Box>
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'sm'}>
{item.intro}
</Box>
</Box>
</Flex>
))
)}
</Box>
</Box>
</Flex>
</>
);
};
export default React.memo(ModuleTemplateList);

View File

@@ -1,58 +0,0 @@
import React from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from 'reactflow';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
const ButtonEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data
}: EdgeProps<{
onDelete: (id: string) => void;
}>) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<EdgeLabelRenderer>
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'20px'}
h={'20px'}
bg={'white'}
borderRadius={'20px'}
color={'black'}
cursor={'pointer'}
border={'1px solid #fff'}
_hover={{
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
}}
onClick={() => data?.onDelete(id)}
>
<MyIcon name="closeSolid" w={'100%'} color={'myGray.600'}></MyIcon>
</Flex>
</EdgeLabelRenderer>
</>
);
};
export default React.memo(ButtonEdge);

View File

@@ -1,13 +0,0 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Box px={4} py={3} position={'relative'} {...props}>
{children}
</Box>
);
};
export default React.memo(Container);

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
const Divider = ({ text }: { text: 'Input' | 'Output' | string }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box
textAlign={'center'}
bg={'#f8f8f8'}
py={2}
borderTop={theme.borders.base}
borderBottom={theme.borders.base}
fontSize={'lg'}
>
{t(`common.${text}`)}
</Box>
);
};
export default React.memo(Divider);

View File

@@ -1,74 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
Flex,
Switch,
Input
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@/types/app';
import { useForm } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
const ExtractFieldModal = ({
defaultField = {
desc: '',
key: '',
required: true
},
onClose,
onSubmit
}: {
defaultField?: ContextExtractAgentItemType;
onClose: () => void;
onSubmit: (data: ContextExtractAgentItemType) => void;
}) => {
const { register, handleSubmit } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField
});
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="姓名/年龄/sql语句……"
{...register('desc', { required: '字段描述不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="name/age/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ExtractFieldModal);

View File

@@ -1,30 +0,0 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@/components/MyTooltip';
const Label = ({
required = false,
children,
description
}: {
required?: boolean;
children: React.ReactNode | string;
description?: string;
}) => (
<Box as={'label'} display={'inline-block'} position={'relative'}>
{children}
{required && (
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
*
</Box>
)}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
</MyTooltip>
)}
</Box>
);
export default React.memo(Label);

View File

@@ -1,135 +0,0 @@
import React, { useMemo } from 'react';
import { Box, Flex, useTheme, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import Avatar from '@/components/Avatar';
import type { FlowModuleItemType } from '@/types/core/app/flow';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowStore, onChangeNode } from '../Provider';
type Props = FlowModuleItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
};
const NodeCard = (props: Props) => {
const {
children,
logo = '/icon/logo.svg',
name = '未知模块',
description,
minW = '300px',
moduleId
} = props;
const { onCopyNode, onDelNode } = useFlowStore();
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const menuList = useMemo(
() => [
{
icon: 'edit',
label: t('common.Rename'),
onClick: () =>
onOpenModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
moduleId,
type: 'attr',
key: 'name',
value: e
});
}
})
},
{
icon: 'copy',
label: t('common.Copy'),
onClick: () => onCopyNode(moduleId)
},
{
icon: 'delete',
label: t('common.Delete'),
onClick: () => onDelNode(moduleId)
},
{
icon: 'back',
label: t('common.Close'),
onClick: () => {}
}
],
[moduleId, name, onChangeNode, onCopyNode, onDelNode, onOpenModal, t, toast]
);
return (
<Box
minW={minW}
maxW={'500px'}
bg={'white'}
border={theme.borders.md}
borderRadius={'md'}
boxShadow={'sm'}
>
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
<Avatar src={logo} borderRadius={'md'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
{name}
</Box>
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon
display={['none', 'inline']}
transform={'translateY(1px)'}
mb={'1px'}
ml={1}
/>
</MyTooltip>
)}
<Box flex={1} />
<Menu autoSelect={false} isLazy>
<MenuButton
className={'nodrag'}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
borderRadius={'md'}
onClick={(e) => {
e.stopPropagation();
}}
>
<MyIcon name={'more'} w={'14px'} p={2} />
</MenuButton>
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
{menuList.map((item) => (
<MenuItem key={item.label} onClick={item.onClick} py={[2, 3]}>
<MyIcon name={item.icon as any} w={['14px', '16px']} />
<Box ml={[1, 2]}>{item.label}</Box>
</MenuItem>
))}
</MenuList>
</Menu>
</Flex>
{children}
<EditTitleModal />
</Box>
);
};
export default React.memo(NodeCard);

View File

@@ -1,115 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
FormControl
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
import { FlowInputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { useTranslation } from 'react-i18next';
import MySelect from '@/components/Select';
import type { FlowInputItemType } from '@/types/core/app/flow';
const typeSelectList = [
{
label: '字符串',
value: FlowValueTypeEnum.string
},
{
label: '数字',
value: FlowValueTypeEnum.number
},
{
label: '布尔',
value: FlowValueTypeEnum.boolean
},
{
label: '任意',
value: FlowValueTypeEnum.any
}
];
const SetInputFieldModal = ({
defaultField = {
label: '',
key: '',
type: FlowInputItemTypeEnum.target,
valueType: FlowValueTypeEnum.string,
description: '',
required: false
},
onClose,
onSubmit
}: {
defaultField?: FlowInputItemType;
onClose: () => void;
onSubmit: (data: FlowInputItemType) => void;
}) => {
const { t } = useTranslation();
const { register, getValues, setValue, handleSubmit } = useForm<FlowInputItemType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
{t('app.Input Field Settings')}
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<MySelect
w={'288px'}
list={typeSelectList}
value={getValues('valueType')}
onchange={(e: any) => {
setValue('valueType', e);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="预约字段/sql语句……"
{...register('label', { required: '字段名不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="appointment/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(SetInputFieldModal);

View File

@@ -1,94 +0,0 @@
import React, { useMemo, useState } from 'react';
import { Box, Button, ModalHeader, ModalFooter, ModalBody, Flex, Input } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
import { useTranslation } from 'react-i18next';
import MySelect from '@/components/Select';
import { FlowOutputItemType } from '@/types/core/app/flow';
const typeSelectList = [
{
label: '字符串',
value: FlowValueTypeEnum.string
},
{
label: '数字',
value: FlowValueTypeEnum.number
},
{
label: '布尔',
value: FlowValueTypeEnum.boolean
},
{
label: '任意',
value: FlowValueTypeEnum.any
}
];
const SetInputFieldModal = ({
defaultField,
onClose,
onSubmit
}: {
defaultField: FlowOutputItemType;
onClose: () => void;
onSubmit: (data: FlowOutputItemType) => void;
}) => {
const { t } = useTranslation();
const { register, getValues, setValue, handleSubmit } = useForm<FlowOutputItemType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
{t('app.Output Field Settings')}
</ModalHeader>
<ModalBody>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<MySelect
w={'288px'}
list={typeSelectList}
value={getValues('valueType')}
onchange={(e: any) => {
setValue('valueType', e);
setRefresh(!refresh);
}}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Input
placeholder="预约字段/sql语句……"
{...register('label', { required: '字段名不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}> key</Box>
<Input
placeholder="appointment/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(SetInputFieldModal);

View File

@@ -1,597 +0,0 @@
import React, { useMemo, useState } from 'react';
import type { FlowInputItemType, SelectAppItemType } from '@/types/core/app/flow';
import {
Box,
Textarea,
Input,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Flex,
useDisclosure,
Button,
useTheme,
Grid
} from '@chakra-ui/react';
import { FlowInputItemTypeEnum } from '@/constants/flow';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import dynamic from 'next/dynamic';
import { onChangeNode, useFlowStore } from '../Provider';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
import TargetHandle from './TargetHandle';
import MyIcon from '@/components/Icon';
import { useTranslation } from 'react-i18next';
import { AIChatProps } from '@/types/core/aiChat';
import { chatModelList } from '@/web/common/system/staticData';
import { formatPrice } from '@fastgpt/global/common/bill/tools';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@/types/core/dataset';
import { useQuery } from '@tanstack/react-query';
import { LLMModelItemType } from '@/types/model';
const SetInputFieldModal = dynamic(() => import('../modules/SetInputFieldModal'));
const SelectAppModal = dynamic(() => import('../../../SelectAppModal'));
const AIChatSettingsModal = dynamic(() => import('../../../AIChatSettingsModal'));
const DatasetSelectModal = dynamic(() => import('../../../DatasetSelectModal'));
export const Label = React.memo(function Label({
moduleId,
inputKey,
...item
}: FlowInputItemType & {
moduleId: string;
inputKey: string;
}) {
const { required = false, description, edit, label, type, valueType } = item;
const [editField, setEditField] = useState<FlowInputItemType>();
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{label}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
{required && (
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
{(type === FlowInputItemTypeEnum.target || valueType) && (
<TargetHandle handleKey={inputKey} valueType={valueType} />
)}
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
...item,
key: inputKey
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField && (
<SetInputFieldModal
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(data) => {
// same key
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'inputs',
key: inputKey,
value: data
});
} else {
// diff key. del and add
onChangeNode({
moduleId,
type: 'addInput',
key: data.key,
value: data
});
setTimeout(() => {
onChangeNode({
moduleId,
type: 'delInput',
key: editField.key,
value: ''
});
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
});
const RenderInput = ({
flowInputList,
moduleId,
CustomComponent = {}
}: {
flowInputList: FlowInputItemType[];
moduleId: string;
CustomComponent?: Record<string, (e: FlowInputItemType) => React.ReactNode>;
}) => {
const sortInputs = useMemo(
() => flowInputList.sort((a, b) => (a.key === FlowInputItemTypeEnum.switch ? -1 : 1)),
[flowInputList]
);
return (
<>
{sortInputs.map(
(item) =>
item.type !== FlowInputItemTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
{!!item.label && <Label moduleId={moduleId} inputKey={item.key} {...item} />}
<Box mt={2} className={'nodrag'}>
{item.type === FlowInputItemTypeEnum.numberInput && (
<NumberInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.input && (
<TextInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.textarea && (
<TextareaRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.select && (
<SelectRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.slider && (
<SliderRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.selectApp && (
<SelectAppRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.aiSettings && (
<AISetting inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.maxToken && (
<MaxTokenRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.selectChatModel && (
<SelectChatModelRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.selectDataset && (
<SelectDatasetRender item={item} moduleId={moduleId} />
)}
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
<>{CustomComponent[item.key]({ ...item })}</>
)}
</Box>
</Box>
)
)}
</>
);
};
export default React.memo(RenderInput);
type RenderProps = {
inputs?: FlowInputItemType[];
item: FlowInputItemType;
moduleId: string;
};
var NumberInputRender = React.memo(function NumberInputRender({ item, moduleId }: RenderProps) {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
});
var TextInputRender = React.memo(function TextInputRender({ item, moduleId }: RenderProps) {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
});
var TextareaRender = React.memo(function TextareaRender({ item, moduleId }: RenderProps) {
return (
<Textarea
rows={5}
placeholder={item.placeholder}
resize={'both'}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
});
var SelectRender = React.memo(function SelectRender({ item, moduleId }: RenderProps) {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
});
var SliderRender = React.memo(function SliderRender({ item, moduleId }: RenderProps) {
return (
<Box pt={5} pb={4} px={2}>
<MySlider
markList={item.markList}
width={'100%'}
min={item.min || 0}
max={item.max}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
});
var AISetting = React.memo(function AISetting({ inputs = [], moduleId }: RenderProps) {
const { t } = useTranslation();
const chatModulesData = useMemo(() => {
const obj: Record<string, any> = {};
inputs.forEach((item) => {
obj[item.key] = item.value;
});
return obj as AIChatProps;
}, [inputs]);
const {
isOpen: isOpenAIChatSetting,
onOpen: onOpenAIChatSetting,
onClose: onCloseAIChatSetting
} = useDisclosure();
return (
<>
<Button
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
onClick={onOpenAIChatSetting}
>
{t('app.AI Settings')}
</Button>
{isOpenAIChatSetting && (
<AIChatSettingsModal
isAdEdit
onClose={onCloseAIChatSetting}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'inputs',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
onCloseAIChatSetting();
}}
defaultData={chatModulesData}
/>
)}
</>
);
});
var MaxTokenRender = React.memo(function MaxTokenRender({
inputs = [],
item,
moduleId
}: RenderProps) {
const model = inputs.find((item) => item.key === 'model')?.value;
const modelData = chatModelList.find((item) => item.model === model);
const maxToken = modelData ? modelData.maxToken : 4000;
const markList = [
{ label: '100', value: 100 },
{ label: `${maxToken}`, value: maxToken }
];
return (
<Box pt={5} pb={4} px={2}>
<MySlider
markList={markList}
width={'100%'}
min={item.min || 100}
max={maxToken}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
});
var SelectChatModelRender = React.memo(function SelectChatModelRender({
inputs = [],
item,
moduleId
}: RenderProps) {
const modelList = (item.customData?.() as LLMModelItemType[]) || chatModelList || [];
function onChangeModel(e: string) {
{
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: {
...item,
value: e
}
});
// update max tokens
const model = modelList.find((item) => item.model === e) || modelList[0];
if (!model) return;
onChangeNode({
moduleId,
type: 'inputs',
key: 'maxToken',
value: {
...inputs.find((input) => input.key === 'maxToken'),
markList: [
{ label: '100', value: 100 },
{ label: `${model.maxToken}`, value: model.maxToken }
],
max: model.maxToken,
value: model.maxToken / 2
}
});
}
}
const list = modelList.map((item) => {
const priceStr = `(${formatPrice(item.price, 1000)}元/1k Tokens)`;
return {
value: item.model,
label: `${item.name}${priceStr}`
};
});
if (!item.value && list.length > 0) {
onChangeModel(list[0].value);
}
return <MySelect width={'100%'} value={item.value} list={list} onchange={onChangeModel} />;
});
var SelectDatasetRender = React.memo(function SelectDatasetRender({ item, moduleId }: RenderProps) {
const theme = useTheme();
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const showKbList = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value.find((kb) => kb.datasetId === dataset._id));
}, [allDatasets, item.value]);
useQuery(['loadAllDatasets'], loadAllDatasets);
return (
<>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} w={'100%'}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
{showKbList.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
<DatasetSelectModal
isOpen={isOpenKbSelect}
activeDatasets={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
key: item.key,
type: 'inputs',
value: {
...item,
value: e
}
});
}}
onClose={onCloseKbSelect}
/>
</>
);
});
var SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }: RenderProps) {
const { appId } = useFlowStore();
const theme = useTheme();
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'base'} w={'100%'}>
</Button>
) : (
<Flex alignItems={'center'} border={theme.borders.base} borderRadius={'md'} px={3} py={2}>
<Avatar src={value?.logo} />
<Box fontWeight={'bold'} ml={1}>
{value?.name}
</Box>
</Flex>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterApps={[appId]}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: 'app',
value: {
...item,
value: e[0]
}
});
}}
/>
)}
</>
);
});

View File

@@ -1,159 +0,0 @@
import React, { useMemo, useState } from 'react';
import type { FlowOutputItemType } from '@/types/core/app/flow';
import { Box, Flex } from '@chakra-ui/react';
import { FlowOutputItemTypeEnum } from '@/constants/flow';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from './SourceHandle';
import MyIcon from '@/components/Icon';
import dynamic from 'next/dynamic';
const SetOutputFieldModal = dynamic(() => import('../modules/SetOutputFieldModal'));
import { onChangeNode } from '../Provider';
import { SystemOutputEnum } from '@/constants/app';
const Label = ({
moduleId,
outputKey,
outputs,
...item
}: FlowOutputItemType & {
outputKey: string;
moduleId: string;
outputs: FlowOutputItemType[];
}) => {
const { label, description, edit } = item;
const [editField, setEditField] = useState<FlowOutputItemType>();
return (
<Flex
className="nodrag"
cursor={'default'}
justifyContent={'right'}
alignItems={'center'}
position={'relative'}
>
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
...item,
key: outputKey
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.filter((output) => output.key !== outputKey)
});
}}
/>
</>
)}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box>{label}</Box>
{!!editField && (
<SetOutputFieldModal
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(data) => {
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: outputs.map((output) => (output.key === outputKey ? data : output))
});
} else {
let index = 0;
const storeOutputs = outputs.filter((output, i) => {
if (output.key !== editField.key) {
return true;
}
index = i;
return false;
});
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: storeOutputs
});
setTimeout(() => {
storeOutputs.splice(index, 0, data);
console.log(index, storeOutputs);
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: [...storeOutputs]
});
}, 10);
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
const RenderOutput = ({
moduleId,
flowOutputList
}: {
moduleId: string;
flowOutputList: FlowOutputItemType[];
}) => {
const sortOutput = useMemo(
() =>
[...flowOutputList].sort((a, b) => {
if (a.key === SystemOutputEnum.finish) return -1;
if (b.key === SystemOutputEnum.finish) return 1;
return 0;
}),
[flowOutputList]
);
return (
<>
{sortOutput.map(
(item) =>
item.type !== FlowOutputItemTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
<Label moduleId={moduleId} outputKey={item.key} outputs={sortOutput} {...item} />
<Box mt={FlowOutputItemTypeEnum.answer ? 0 : 2} className={'nodrag'}>
{item.type === FlowOutputItemTypeEnum.source && (
<SourceHandle handleKey={item.key} valueType={item.valueType} />
)}
</Box>
</Box>
)
)}
</>
);
};
export default React.memo(RenderOutput);

View File

@@ -1,45 +0,0 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, Position } from 'reactflow';
import { FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
import MyTooltip from '@/components/MyTooltip';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${FlowValueTypeEnum}`;
}
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[FlowValueTypeEnum.any] as any),
[valueType]
);
return (
<Box
position={'absolute'}
top={'50%'}
right={'-16px'}
transform={'translate(50%,-50%)'}
{...props}
>
<MyTooltip label={`${valueType}类型`}>
<Handle
style={{
width: '12px',
height: '12px',
...valueStyle
}}
type="source"
id={handleKey}
position={Position.Right}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(SourceHandle);

View File

@@ -1,47 +0,0 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, OnConnect, Position } from 'reactflow';
import { FlowValueTypeEnum, FlowValueTypeStyle } from '@/constants/flow';
import MyTooltip from '@/components/MyTooltip';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${FlowValueTypeEnum}`;
onConnect?: OnConnect;
}
const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[FlowValueTypeEnum.any] as any),
[valueType]
);
return (
<Box
key={handleKey}
position={'absolute'}
top={'50%'}
left={'-16px'}
transform={'translate(50%,-50%)'}
{...props}
>
<MyTooltip label={`${valueType}类型`}>
<Handle
style={{
width: '12px',
height: '12px',
...valueStyle
}}
type="target"
id={handleKey}
position={Position.Left}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(TargetHandle);

View File

@@ -1,145 +1,60 @@
import React, { useEffect } from 'react';
import ReactFlow, { Background, Controls, ReactFlowProvider } from 'reactflow';
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { edgeOptions, connectionLineStyle, FlowModuleTypeEnum } from '@/constants/flow';
import type { AppSchema } from '@/types/mongoSchema';
import React, { useMemo } from 'react';
import { AppSchema } from '@/types/mongoSchema';
import Header from './Header';
import Flow from '@/components/core/module/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { SystemModuleTemplateType } from '@fastgpt/global/core/module/type.d';
import { SystemModuleTemplates } from '@/constants/flow/ModuleTemplate';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { usePluginStore } from '@/web/core/plugin/store/plugin';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
type Props = { app: AppSchema; onClose: () => void };
import ButtonEdge from './components/modules/ButtonEdge';
import TemplateList from './components/TemplateList';
import FlowProvider, { useFlowStore } from './components/Provider';
import Header from './components/Header';
const Render = ({ app, onClose }: Props) => {
const { nodes } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginModuleTemplates } = usePluginStore();
import 'reactflow/dist/style.css';
const filterTemplates = useMemo(() => {
const copyTemplates: SystemModuleTemplateType = JSON.parse(
JSON.stringify(SystemModuleTemplates)
);
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1
};
// filter some template
nodes.forEach((node) => {
if (node.type && filterType[node.type]) {
copyTemplates.forEach((item) => {
item.list.forEach((module, index) => {
if (module.flowType === node.type) {
item.list.splice(index, 1);
}
});
});
}
});
const NodeChat = dynamic(() => import('./components/Nodes/NodeChat'));
const NodeDatasetSearch = dynamic(() => import('./components/Nodes/NodeDatasetSearch'));
const NodeHistory = dynamic(() => import('./components/Nodes/NodeHistory'));
const NodeTFSwitch = dynamic(() => import('./components/Nodes/NodeTFSwitch'));
const NodeAnswer = dynamic(() => import('./components/Nodes/NodeAnswer'));
const NodeQuestionInput = dynamic(() => import('./components/Nodes/NodeQuestionInput'));
const NodeCQNode = dynamic(() => import('./components/Nodes/NodeCQNode'));
const NodeVariable = dynamic(() => import('./components/Nodes/NodeVariable'));
const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'));
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'));
const NodeHttp = dynamic(() => import('./components/Nodes/NodeHttp'));
const NodeAPP = dynamic(() => import('./components/Nodes/NodeAPP'));
return copyTemplates;
}, [nodes]);
const nodeTypes = {
[FlowModuleTypeEnum.userGuide]: NodeUserGuide,
[FlowModuleTypeEnum.variable]: NodeVariable,
[FlowModuleTypeEnum.questionInput]: NodeQuestionInput,
[FlowModuleTypeEnum.historyNode]: NodeHistory,
[FlowModuleTypeEnum.chatNode]: NodeChat,
[FlowModuleTypeEnum.datasetSearchNode]: NodeDatasetSearch,
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
[FlowModuleTypeEnum.contentExtract]: NodeExtract,
[FlowModuleTypeEnum.httpRequest]: NodeHttp,
[FlowModuleTypeEnum.app]: NodeAPP
// [FlowModuleTypeEnum.empty]: EmptyModule
};
const edgeTypes = {
buttonedge: ButtonEdge
};
type Props = { app: AppSchema; onCloseSettings: () => void };
const AppEdit = React.memo(function AppEdit(props: Props) {
const { app } = props;
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
const { reactFlowWrapper, nodes, onNodesChange, edges, onEdgesChange, onConnect, initData } =
useFlowStore();
useEffect(() => {
initData(JSON.parse(JSON.stringify(app.modules)));
}, [app.modules]);
useQuery(['getUserPlugs2ModuleTemplates'], () => loadPluginModuleTemplates());
return (
<>
{/* header */}
<Header app={app} onCloseSettings={props.onCloseSettings} />
<Box
minH={'400px'}
flex={'1 0 0'}
w={'100%'}
h={0}
position={'relative'}
onContextMenu={(e) => {
e.preventDefault();
return false;
}}
>
{/* open module template */}
<IconButton
position={'absolute'}
top={5}
left={5}
w={'38px'}
h={'38px'}
borderRadius={'50%'}
icon={<SmallCloseIcon fontSize={'26px'} />}
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<Flow
systemTemplates={filterTemplates}
pluginTemplates={[{ label: '', list: pluginModuleTemplates }]}
show2Plugin
modules={app.modules}
Header={<Header app={app} onClose={onClose} />}
/>
);
};
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={edgeOptions}
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={(connect) => {
connect.sourceHandle &&
connect.targetHandle &&
onConnect({
connect
});
}}
>
<Background />
<Controls position={'bottom-right'} style={{ display: 'flex' }} showInteractive={false} />
</ReactFlow>
<TemplateList isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</Box>
</>
export default React.memo(function AdEdit(props: Props) {
return (
<FlowProvider filterAppIds={[props.app._id]}>
<Render {...props} />
</FlowProvider>
);
});
const Flow = (data: Props) => {
return (
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
<ReactFlowProvider>
<FlowProvider appId={data?.app?._id}>
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
{!!data.app._id && <AppEdit {...data} />}
</Flex>
</FlowProvider>
</ReactFlowProvider>
</Box>
);
};
export default React.memo(Flow);

View File

@@ -37,10 +37,11 @@ import {
welcomeTextTip,
questionGuideTip
} from '@/constants/flow/ModuleTemplate';
import { AppModuleItemType, VariableItemType } from '@/types/app';
import { VariableItemType } from '@/types/app';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { FlowModuleTypeEnum } from '@/constants/flow';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { streamFetch } from '@/web/common/api/fetch';
import { useRouter } from 'next/router';
import { useToast } from '@/web/common/hooks/useToast';
@@ -56,15 +57,15 @@ import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { addVariable } from '../VariableEditModal';
import { KbParamsModal } from '../DatasetSelectModal';
import { addVariable } from '@/components/core/module/VariableEditModal';
import { KbParamsModal } from '@/components/core/module/DatasetSelectModal';
import { AppTypeEnum } from '@/constants/app';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
const VariableEditModal = dynamic(() => import('../VariableEditModal'));
const VariableEditModal = dynamic(() => import('@/components/core/module/VariableEditModal'));
const InfoModal = dynamic(() => import('../InfoModal'));
const DatasetSelectModal = dynamic(() => import('../DatasetSelectModal'));
const AIChatSettingsModal = dynamic(() => import('../AIChatSettingsModal'));
const DatasetSelectModal = dynamic(() => import('@/components/core/module/DatasetSelectModal'));
const AIChatSettingsModal = dynamic(() => import('@/components/core/module/AIChatSettingsModal'));
const Settings = ({ appId }: { appId: string }) => {
const theme = useTheme();
@@ -596,13 +597,13 @@ const ChatTest = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { appDetail, userInfo } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const [modules, setModules] = useState<AppModuleItemType[]>([]);
const [modules, setModules] = useState<ModuleItemType[]>([]);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
const historyMaxLen =
modules
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
?.find((item) => item.flowType === FlowNodeTypeEnum.historyNode)
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
const history = chatList.slice(-historyMaxLen - 2, -2);

View File

@@ -1,325 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Card,
Flex,
Box,
Button,
ModalBody,
ModalFooter,
useTheme,
Textarea,
Grid,
Divider
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import { useForm } from 'react-hook-form';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { SelectedDatasetType } from '@/types/core/dataset';
import { useToast } from '@/web/common/hooks/useToast';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
import MyModal from '@/components/MyModal';
import MyIcon from '@/components/Icon';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constant';
import { useTranslation } from 'react-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { feConfigs } from '@/web/common/system/staticData';
import DatasetSelectContainer, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
export type KbParamsType = {
searchSimilarity: number;
searchLimit: number;
searchEmptyText: string;
};
export const DatasetSelectModal = ({
isOpen,
activeDatasets = [],
onChange,
onClose
}: {
isOpen: boolean;
activeDatasets: SelectedDatasetType;
onChange: (e: SelectedDatasetType) => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { allDatasets } = useDatasetStore();
const [selectedKbList, setSelectedKbList] = useState<SelectedDatasetType>(
activeDatasets.filter((dataset) => {
return allDatasets.find((item) => item._id === dataset.datasetId);
})
);
const { toast } = useToast();
const { paths, parentId, setParentId, datasets } = useDatasetSelect();
const filterKbList = useMemo(() => {
return {
selected: allDatasets.filter((item) =>
selectedKbList.find((dataset) => dataset.datasetId === item._id)
),
unSelected: datasets.filter(
(item) => !selectedKbList.find((dataset) => dataset.datasetId === item._id)
)
};
}, [datasets, allDatasets, selectedKbList]);
return (
<DatasetSelectContainer
isOpen={isOpen}
paths={paths}
parentId={parentId}
setParentId={setParentId}
tips={'仅能选择同一个索引模型的知识库'}
onClose={onClose}
>
<Flex h={'100%'} flexDirection={'column'} flex={'1 0 0'}>
<ModalBody flex={'1 0 0'} overflowY={'auto'} userSelect={'none'}>
<Grid
gridTemplateColumns={[
'repeat(1, minmax(0, 1fr))',
'repeat(2, minmax(0, 1fr))',
'repeat(3, minmax(0, 1fr))'
]}
gridGap={3}
>
{filterKbList.selected.map((item) =>
(() => {
return (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
bg={'myBlue.300'}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box flex={'1 0 0'} w={0} className="textEllipsis" mx={3}>
{item.name}
</Box>
<MyIcon
name={'delete'}
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
onClick={() => {
setSelectedKbList((state) =>
state.filter((kb) => kb.datasetId !== item._id)
);
}}
/>
</Flex>
</Card>
);
})()
)}
</Grid>
{filterKbList.selected.length > 0 && <Divider my={3} />}
<Grid
gridTemplateColumns={[
'repeat(1, minmax(0, 1fr))',
'repeat(2, minmax(0, 1fr))',
'repeat(3, minmax(0, 1fr))'
]}
gridGap={3}
>
{filterKbList.unSelected.map((item) =>
(() => {
return (
<MyTooltip
key={item._id}
label={
item.type === DatasetTypeEnum.dataset
? t('dataset.Select Dataset')
: t('dataset.Select Folder')
}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
if (item.type === DatasetTypeEnum.folder) {
setParentId(item._id);
} else if (item.type === DatasetTypeEnum.dataset) {
const vectorModel = selectedKbList[0]?.vectorModel?.model;
if (vectorModel && vectorModel !== item.vectorModel.model) {
return toast({
status: 'warning',
title: '仅能选择同一个索引模型的知识库'
});
}
setSelectedKbList((state) => [
...state,
{ datasetId: item._id, vectorModel: item.vectorModel }
]);
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box
flex={'1 0 0'}
w={0}
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
{item.type === DatasetTypeEnum.folder ? (
<Box color={'myGray.500'}>{t('Folder')}</Box>
) : (
<>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</>
)}
</Flex>
</Card>
</MyTooltip>
);
})()
)}
</Grid>
{filterKbList.unSelected.length === 0 && (
<Flex mt={5} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} mt={'20vh'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
西~
</Box>
</Flex>
)}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
// filter out the dataset that is not in the kList
const filterKbList = selectedKbList.filter((dataset) => {
return allDatasets.find((item) => item._id === dataset.datasetId);
});
onClose();
onChange(filterKbList);
}}
>
</Button>
</ModalFooter>
</Flex>
</DatasetSelectContainer>
);
};
export const KbParamsModal = ({
searchEmptyText,
searchLimit,
searchSimilarity,
onClose,
onChange
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
const [refresh, setRefresh] = useState(false);
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
defaultValues: {
searchEmptyText,
searchLimit,
searchSimilarity
}
});
return (
<MyModal isOpen={true} onClose={onClose} title={'搜索参数调整'} minW={['90vw', '600px']}>
<Flex flexDirection={'column'}>
<ModalBody>
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
<Box flex={'0 0 100px'} mb={[8, 0]}>
<MyTooltip
label={'不同索引模型的相似度有区别,请通过搜索测试来选择合适的数值'}
forceShow
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Box>
<MySlider
markList={[
{ label: '0', value: 0 },
{ label: '1', value: 1 }
]}
min={0}
max={1}
step={0.01}
value={getValues('searchSimilarity')}
onChange={(val) => {
setValue('searchSimilarity', val);
setRefresh(!refresh);
}}
/>
</Box>
<Box display={['block', 'flex']} py={8}>
<Box flex={'0 0 100px'} mb={[8, 0]}>
</Box>
<Box flex={1}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: '20', value: 20 }
]}
min={1}
max={20}
value={getValues('searchLimit')}
onChange={(val) => {
setValue('searchLimit', val);
setRefresh(!refresh);
}}
/>
</Box>
</Box>
<Box display={['block', 'flex']} pt={3}>
<Box flex={'0 0 100px'} mb={[2, 0]}>
</Box>
<Box flex={1}>
<Textarea
rows={5}
maxLength={500}
placeholder={`若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文${feConfigs?.systemTitle} 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。`}
{...register('searchEmptyText')}
></Textarea>
</Box>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button
onClick={() => {
onClose();
handleSubmit(onChange)();
}}
>
</Button>
</ModalFooter>
</Flex>
</MyModal>
);
};
export default DatasetSelectModal;

View File

@@ -48,8 +48,7 @@ const Logs = ({ appId }: { appId: string }) => {
data: logs,
isLoading,
Pagination,
getData,
pageNum
getData
} = usePagination<AppLogsListItemType>({
api: getAppChatLogs,
pageSize: 20,
@@ -178,7 +177,7 @@ const Logs = ({ appId }: { appId: string }) => {
);
};
export default Logs;
export default React.memo(Logs);
function DetailLogsModal({
appId,

View File

@@ -1,10 +1,15 @@
import React, { useEffect, useState } from 'react';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useTranslation } from 'react-i18next';
import { Box } from '@chakra-ui/react';
const API = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
return <ApiKeyTable tips={t('openapi.app key tips')} appId={appId} />;
return (
<Box pt={3}>
<ApiKeyTable tips={t('openapi.app key tips')} appId={appId} />
</Box>
);
};
export default API;

View File

@@ -59,9 +59,9 @@ const Share = ({ appId }: { appId: string }) => {
} = useQuery(['initShareChatList', appId], () => getShareChatList(appId));
return (
<Box position={'relative'} pt={[3, 5, 8]} px={[2, 8]} minH={'50vh'}>
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<Box fontWeight={'bold'} fontSize={['md', 'xl']}>
<MyTooltip
forceShow
@@ -248,8 +248,6 @@ function EditLinkModal({
onEdit: () => void;
}) {
const { t } = useTranslation();
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const {
register,
setValue,
@@ -258,6 +256,8 @@ function EditLinkModal({
defaultValues: defaultData
});
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: OutLinkEditType) =>
createShareChat({

View File

@@ -1,110 +0,0 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { getMyModels } from '@/web/core/app/api';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@/types/core/app/flow';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'react-i18next';
import { useLoading } from '@/web/common/hooks/useLoading';
const SelectAppModal = ({
defaultApps = [],
filterApps = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
filterApps?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
}) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { data = [], isLoading } = useQuery(['loadMyApos'], () => getMyModels());
const apps = useMemo(
() => data.filter((app) => !filterApps.includes(app._id)),
[data, filterApps]
);
return (
<MyModal
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
onClose={onClose}
w={'700px'}
position={'relative'}
>
<ModalBody
minH={'300px'}
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3,1fr)']}
gridGap={4}
>
{apps.map((app) => (
<Flex
key={app._id}
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
px={1}
py={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {
bg: 'myBlue.200',
onClick: () => {
setSelectedApps(selectedApps.filter((e) => e !== app._id));
}
}
: {
onClick: () => {
if (max === 1) {
setSelectedApps([app._id]);
} else if (selectedApps.length < max) {
setSelectedApps([...selectedApps, app._id]);
}
}
})}
>
<Avatar src={app.avatar} w={['16px', '22px']} />
<Box fontWeight={'bold'} ml={1}>
{app.name}
</Box>
</Flex>
))}
</ModalBody>
<ModalFooter>
<Button variant={'base'} onClick={onClose}>
{t('Cancel')}
</Button>
<Button
ml={2}
onClick={() => {
onSuccess(
apps
.filter((app) => selectedApps.includes(app._id))
.map((app) => ({
id: app._id,
name: app.name,
logo: app.avatar
}))
);
onClose();
}}
>
{t('Confirm')}
</Button>
</ModalFooter>
<Loading loading={isLoading} fixed={false} />
</MyModal>
);
};
export default React.memo(SelectAppModal);

View File

@@ -1,209 +0,0 @@
import React, { useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Flex,
Switch,
Input,
Grid,
FormControl,
useTheme
} from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { VariableInputEnum } from '@/constants/app';
import type { VariableItemType } from '@/types/app';
import MyIcon from '@/components/Icon';
import { useForm } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
const VariableTypeList = [
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
];
export type VariableFormType = {
variable: VariableItemType;
};
const VariableEditModal = ({
defaultVariable,
onClose,
onSubmit
}: {
defaultVariable: VariableItemType;
onClose: () => void;
onSubmit: (data: VariableFormType) => void;
}) => {
const theme = useTheme();
const [refresh, setRefresh] = useState(false);
const { reset, getValues, setValue, register, control, handleSubmit } = useForm<VariableFormType>(
{
defaultValues: {
variable: defaultVariable
}
}
);
const {
fields: selectEnums,
append: appendEnums,
remove: removeEnums
} = useFieldArray({
control,
name: 'variable.enums'
});
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'}>
<MyIcon name={'variable'} mr={2} w={'24px'} color={'#FF8A4C'} />
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box w={'70px'}></Box>
<Switch {...register('variable.required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}></Box>
<Input {...register('variable.label', { required: '变量名不能为空' })} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}> key</Box>
<Input {...register('variable.key', { required: '变量 key 不能为空' })} />
</Flex>
<Box mt={5} mb={2}>
</Box>
<Grid gridTemplateColumns={'repeat(2,130px)'} gridGap={4}>
{VariableTypeList.map((item) => (
<Flex
key={item.key}
px={4}
py={1}
border={theme.borders.base}
borderRadius={'md'}
cursor={'pointer'}
{...(item.key === getValues('variable.type')
? {
bg: 'myWhite.600'
}
: {
_hover: {
boxShadow: 'md'
},
onClick: () => {
setValue('variable.type', item.key);
setRefresh(!refresh);
}
})}
>
<MyIcon name={item.icon as any} w={'16px'} />
<Box ml={3}>{item.label}</Box>
</Flex>
))}
</Grid>
{getValues('variable.type') === VariableInputEnum.input && (
<>
<Box mt={5} mb={2}>
</Box>
<Box>
<NumberInput max={100} min={1} step={1} position={'relative'}>
<NumberInputField
{...register('variable.maxLen', {
min: 1,
max: 100,
valueAsNumber: true
})}
max={100}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Box>
</>
)}
{getValues('variable.type') === VariableInputEnum.select && (
<>
<Box mt={5} mb={2}>
</Box>
<Box>
{selectEnums.map((item, i) => (
<Flex key={item.id} mb={2} alignItems={'center'}>
<FormControl>
<Input
{...register(`variable.enums.${i}.value`, {
required: '选项内容不能为空'
})}
/>
</FormControl>
<MyIcon
ml={3}
name={'delete'}
w={'16px'}
cursor={'pointer'}
p={2}
borderRadius={'lg'}
_hover={{ bg: 'red.100' }}
onClick={() => removeEnums(i)}
/>
</Flex>
))}
</Box>
<Button
variant={'solid'}
w={'100%'}
textAlign={'left'}
leftIcon={<SmallAddIcon />}
bg={'myGray.100 !important'}
onClick={() => appendEnums({ value: '' })}
>
</Button>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(VariableEditModal);
export const defaultVariable: VariableItemType = {
id: nanoid(),
key: 'key',
label: 'label',
type: VariableInputEnum.input,
required: true,
maxLen: 50,
enums: [{ value: '' }]
};
export const addVariable = () => {
const newVariable = { ...defaultVariable, key: nanoid(), id: nanoid() };
return newVariable;
};

View File

@@ -170,7 +170,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
<Box flex={'1 0 0'} h={[0, '100%']} overflow={['overlay', '']}>
{currentTab === TabEnum.basicEdit && <BasicEdit appId={appId} />}
{currentTab === TabEnum.adEdit && appDetail && (
<AdEdit app={appDetail} onCloseSettings={() => setCurrentTab(TabEnum.basicEdit)} />
<AdEdit app={appDetail} onClose={() => setCurrentTab(TabEnum.basicEdit)} />
)}
{currentTab === TabEnum.logs && <Logs appId={appId} />}
{currentTab === TabEnum.outLink && <OutLink appId={appId} />}

View File

@@ -7,7 +7,8 @@ import {
Flex,
IconButton,
Button,
useDisclosure
useDisclosure,
Image
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useUserStore } from '@/web/support/user/useUserStore';
@@ -69,11 +70,14 @@ const MyApps = () => {
return (
<PageContainer>
<Flex pt={3} px={5} alignItems={'center'}>
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
{t('app.My Apps')}
</Box>
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/ai.svg'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={['20px', '24px']} fontWeight={'bold'}>
{t('app.My Apps')}
</Box>
</Flex>
<Button leftIcon={<AddIcon />} variant={'base'} onClick={onOpenCreateModal}>
{t('common.New Create')}
</Button>
</Flex>
<Grid

View File

@@ -24,7 +24,7 @@ const Navbar = () => {
? [
{
label: t('home.Commercial'),
key: 'community',
key: 'Commercial',
onClick: () => {
window.open(
'https://fael3z0zfze.feishu.cn/share/base/form/shrcnRxj3utrzjywsom96Px4sud',

View File

@@ -12,9 +12,7 @@ import {
Image,
MenuButton,
useTheme,
useDisclosure,
ModalFooter,
Button
useDisclosure
} from '@chakra-ui/react';
import {
getDatasetCollections,
@@ -57,7 +55,6 @@ const FileImportModal = dynamic(() => import('./Import/ImportModal'), {});
const CollectionCard = () => {
const BoxRef = useRef<HTMLDivElement>(null);
const theme = useTheme();
const lastSearch = useRef('');
const router = useRouter();
const { toast } = useToast();
@@ -223,7 +220,7 @@ const CollectionCard = () => {
}, [parentId]);
return (
<Box ref={BoxRef} py={[1, 3]} h={'100%'} overflow={'overlay'}>
<Flex flexDirection={'column'} ref={BoxRef} py={[1, 3]} h={'100%'}>
<Flex px={[2, 5]} alignItems={['flex-start', 'center']}>
<Box flex={1}>
<ParentPath
@@ -336,10 +333,12 @@ const CollectionCard = () => {
]}
/>
</Flex>
<TableContainer mt={[0, 3]} position={'relative'} minH={'50vh'}>
<TableContainer mt={[0, 3]} position={'relative'} flex={'1 0 0'} overflowY={'auto'}>
<Table variant={'simple'} fontSize={'sm'} draggable={false}>
<Thead draggable={false}>
<Tr>
<Th>#</Th>
<Th>{t('common.Name')}</Th>
<Th>{t('dataset.collections.Data Amount')}</Th>
<Th>{t('common.Time')}</Th>
@@ -348,7 +347,7 @@ const CollectionCard = () => {
</Tr>
</Thead>
<Tbody>
{formatCollections.map((collection) => (
{formatCollections.map((collection, index) => (
<Tr
key={collection._id}
_hover={{ bg: 'myWhite.600' }}
@@ -408,6 +407,7 @@ const CollectionCard = () => {
}
}}
>
<Td w={'50px'}>{index + 1}</Td>
<Td minW={'150px'} maxW={['200px', '300px']} draggable>
<Flex alignItems={'center'}>
<Image src={collection.icon} w={'16px'} mr={2} alt={''} />
@@ -592,7 +592,7 @@ const CollectionCard = () => {
}}
/>
)}
</Box>
</Flex>
);
};

View File

@@ -73,10 +73,6 @@ const DataCard = () => {
const { data: collection } = useQuery(['getDatasetCollectionById', collectionId], () =>
getDatasetCollectionById(collectionId)
);
const fileIcon = useMemo(
() => getCollectionIcon(collection?.type, collection?.name),
[collection?.name, collection?.type]
);
return (
<Box ref={BoxRef} position={'relative'} px={5} py={[1, 5]} h={'100%'} overflow={'overlay'}>
@@ -100,8 +96,6 @@ const DataCard = () => {
}
/>
<Flex className="textEllipsis" flex={'1 0 0'} mr={[3, 5]} alignItems={'center'}>
<Image src={fileIcon || '/imgs/files/file.svg'} w={['16px', '18px']} mr={2} alt={''} />
<Box lineHeight={1.2}>
<RawSourceText
sourceName={collection?.name}

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { Box, Flex, Button, Textarea, IconButton, BoxProps } from '@chakra-ui/react';
import { Box, Flex, Button, Textarea, IconButton, BoxProps, Image, Link } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import {
postData2Dataset,
@@ -22,10 +22,13 @@ import type { SetOneDatasetDataProps } from '@/global/core/api/datasetReq';
import { useRequest } from '@/web/common/hooks/useRequest';
import { countPromptTokens } from '@/global/common/tiktoken';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { feConfigs } from '@/web/common/system/staticData';
export type RawSourceType = {
sourceName?: string;
sourceId?: string;
addr?: boolean;
};
export type RawSourceTextProps = BoxProps & RawSourceType;
export type InputDataType = SetOneDatasetDataProps & RawSourceType;
@@ -135,7 +138,23 @@ const InputDataModal = ({
<MyModal
isOpen={true}
isCentered
title={defaultValues.id ? t('dataset.data.Update Data') : t('dataset.data.Input Data')}
title={
<Flex alignItems={'flex-end'}>
<Box>
{defaultValues.id ? t('dataset.data.Update Data') : t('dataset.data.Input Data')}
</Box>
<Link
href={`${feConfigs.docUrl}/docs/use-cases/datasetengine`}
target={'_blank'}
fontSize={'sm'}
color={'myGray.600'}
textDecor={'underline'}
ml={2}
>
</Link>
</Flex>
}
w={'90vw'}
maxW={'90vw'}
h={'90vh'}
@@ -152,13 +171,17 @@ const InputDataModal = ({
>
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']}>
<Flex>
<Box h={'30px'}>{'匹配的知识点'}</Box>
<MyTooltip label={'被向量化的部分,通常是问题,也可以是一段陈述描述'}>
<Box h={'25px'}>{'被搜索的内容'}</Box>
<MyTooltip
label={
'被向量化的部分,该部分的质量决定了对话时,能否高效的查找到合适的知识点。\n该内容通常是问题或是一段陈述描述介绍'
}
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea
placeholder={`匹配的知识点。这部分内容会被搜索,请把控内容的质量,最多 ${maxToken} 字。`}
placeholder={`被向量化的部分,该部分的质量决定了对话时,能否高效的查找到合适的知识点。\n该内容通常是问题或是一段陈述描述介绍,最多 ${maxToken} 字。`}
maxLength={maxToken}
resize={'none'}
h={'calc(100% - 30px)'}
@@ -169,16 +192,18 @@ const InputDataModal = ({
</Box>
<Box flex={1} h={['50%', '100%']}>
<Flex>
<Box h={'30px'}>{'补充内容'}</Box>
<Box h={'25px'}>{'补充内容(可选)'}</Box>
<MyTooltip
label={'匹配的知识点被命中后,这部分内容会随匹配知识点一起注入模型,引导模型回答'}
label={
'该部分内容不影响搜索质量。当“被搜索的内容”被搜索到后,“补充内容”可以选择性被填入提示词,从而实现更加丰富的提示词组合。'
}
>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea
placeholder={
'部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,通常是问题的答案。'
'部分内容不影响搜索质量。当“被搜索的内容”被搜索到后,“补充内容”可以选择性被填入提示词,从而实现更加丰富的提示词组合。可以是问题的答案、代码、图片、表格等。'
}
resize={'none'}
h={'calc(100% - 30px)'}
@@ -251,24 +276,30 @@ const InputDataModal = ({
export default InputDataModal;
export function RawSourceText({ sourceId, sourceName = '', ...props }: RawSourceTextProps) {
export function RawSourceText({
sourceId,
sourceName = '',
addr = true,
...props
}: RawSourceTextProps) {
const { t } = useTranslation();
const { toast } = useToast();
const { setLoading } = useSystemStore();
const canPreview = useMemo(() => !!sourceId, [sourceId]);
const canPreview = useMemo(() => !!sourceId && addr, [addr, sourceId]);
const icon = useMemo(() => getSourceNameIcon({ sourceId, sourceName }), [sourceId, sourceName]);
return (
<MyTooltip
label={sourceId ? t('file.Click to view file') || '' : ''}
label={canPreview ? t('file.Click to view file') || '' : ''}
shouldWrapChildren={false}
>
<Box
color={'myGray.600'}
display={'inline-block'}
display={'inline-flex'}
alignItems={'center'}
whiteSpace={'nowrap'}
maxW={['200px', '300px']}
className={'textEllipsis'}
{...(canPreview
? {
cursor: 'pointer',
@@ -292,7 +323,10 @@ export function RawSourceText({ sourceId, sourceName = '', ...props }: RawSource
: {})}
{...props}
>
{sourceName || t('common.Unknow Source')}
<Image src={icon} alt="" w={'14px'} mr={2} />
<Box maxW={['200px', '300px']} className={'textEllipsis'}>
{sourceName || t('common.Unknow Source')}
</Box>
</Box>
</MyTooltip>
);

View File

@@ -155,9 +155,12 @@ const Kb = () => {
))}
</Flex>
) : (
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
</Box>
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/db.png'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
</Box>
</Flex>
)}
<MyMenu

View File

@@ -0,0 +1,161 @@
import React, { useCallback } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { putUpdatePlugin } from '@/web/core/plugin/api';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
const PreviewPlugin = dynamic(() => import('./Preview'));
type Props = { plugin: PluginItemSchema; onClose: () => void };
const Header = ({ plugin, onClose }: Props) => {
const theme = useTheme();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { nodes, edges, onFixView } = useFlowProviderStore();
const [previewModules, setPreviewModules] = React.useState<ModuleItemType[]>();
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: () => {
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
// update custom input connected
if (item.flowType === FlowNodeTypeEnum.pluginInput) {
item.inputs.forEach((item) => {
item.connected = true;
});
if (item.outputs.find((output) => output.targets.length === 0)) {
return Promise.reject(t('module.Plugin input must connect'));
}
}
if (item.inputs.find((input) => input.required && !input.connected)) {
return Promise.reject(`${item.name}】存在未连接的必填输入`);
}
if (item.inputs.find((input) => input.valueCheck && !input.valueCheck(input.value))) {
return Promise.reject(`${item.name}】存在为填写的必填项`);
}
}
// plugin must have input
const pluginInputModule = modules.find(
(item) => item.flowType === FlowNodeTypeEnum.pluginInput
);
if (!pluginInputModule) {
return Promise.reject(t('module.Plugin input is required'));
}
if (pluginInputModule.inputs.length < 1) {
return Promise.reject(t('module.Plugin input is not value'));
}
return putUpdatePlugin({
id: plugin._id,
modules
});
},
successToast: '保存配置成功',
errorToast: '保存配置异常'
});
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
>
<MyTooltip label={'返回'} offset={[10, 10]}>
<IconButton
size={'sm'}
icon={<MyIcon name={'back'} w={'14px'} />}
borderRadius={'md'}
borderColor={'myGray.300'}
variant={'base'}
aria-label={''}
onClick={() => {
onClose();
onFixView();
}}
/>
</MyTooltip>
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
{plugin.name}
</Box>
<MyTooltip label={t('app.Import Configs')}>
<IconButton
mr={[3, 6]}
icon={<MyIcon name={'importLight'} w={['14px', '16px']} />}
borderRadius={'lg'}
variant={'base'}
aria-label={'save'}
onClick={onOpenImport}
/>
</MyTooltip>
<MyTooltip label={t('app.Export Configs')}>
<IconButton
mr={[3, 6]}
icon={<MyIcon name={'export'} w={['14px', '16px']} />}
borderRadius={'lg'}
variant={'base'}
aria-label={'save'}
onClick={() =>
copyData(
JSON.stringify(flowNode2Modules({ nodes, edges }), null, 2),
t('app.Export Config Successful')
)
}
/>
</MyTooltip>
<MyTooltip label={t('module.Preview Plugin')}>
<IconButton
mr={[3, 6]}
icon={<MyIcon name={'core/module/previewLight'} w={['14px', '16px']} />}
borderRadius={'lg'}
aria-label={'save'}
variant={'base'}
onClick={() => {
setPreviewModules(flowNode2Modules({ nodes, edges }));
}}
/>
</MyTooltip>
<MyTooltip label={t('module.Save Config')}>
<IconButton
icon={<MyIcon name={'save'} w={['14px', '16px']} />}
borderRadius={'lg'}
isLoading={isLoading}
aria-label={'save'}
onClick={onclickSave}
/>
</MyTooltip>
</Flex>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
{!!previewModules && (
<PreviewPlugin
plugin={plugin}
modules={previewModules}
onClose={() => setPreviewModules(undefined)}
/>
)}
</>
);
};
export default React.memo(Header);

View File

@@ -0,0 +1,68 @@
import React, { useEffect } from 'react';
import ReactFlow, { Background, ReactFlowProvider, useNodesState } from 'reactflow';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import { formatPluginIOModules } from '@fastgpt/global/core/module/utils';
import MyModal from '@/components/MyModal';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { appModule2FlowNode } from '@/utils/adapt';
const nodeTypes = {
[FlowNodeTypeEnum.pluginModule]: dynamic(
() => import('@/components/core/module/Flow/components/nodes/NodePreviewPlugin')
)
};
const PreviewPlugin = ({
plugin,
modules,
onClose
}: {
plugin: PluginItemSchema;
modules: ModuleItemType[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
useEffect(() => {
setNodes([
appModule2FlowNode({
item: {
moduleId: 'plugin',
flowType: FlowNodeTypeEnum.pluginModule,
logo: plugin.avatar,
name: plugin.name,
description: plugin.intro,
intro: plugin.intro,
...formatPluginIOModules(plugin._id, modules)
}
})
]);
}, [modules, plugin, setNodes]);
return (
<MyModal isOpen title={t('module.Preview Plugin')} onClose={onClose} isCentered>
<Box h={'400px'} w={'400px'}>
<ReactFlowProvider>
<ReactFlow
fitView
nodes={nodes}
edges={[]}
minZoom={0.1}
maxZoom={1.5}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
>
<Background />
</ReactFlow>
</ReactFlowProvider>
</Box>
</MyModal>
);
};
export default React.memo(PreviewPlugin);

View File

@@ -0,0 +1,96 @@
import React, { useMemo } from 'react';
import { useRouter } from 'next/router';
import Header from './Header';
import Flow from '@/components/core/module/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { SystemModuleTemplateType } from '@fastgpt/global/core/module/type.d';
import { PluginModuleTemplates } from '@/constants/flow/ModuleTemplate';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useQuery } from '@tanstack/react-query';
import { getOnePlugin, getUserPlugs2ModuleTemplates } from '@/web/core/plugin/api';
import { useToast } from '@/web/common/hooks/useToast';
import Loading from '@/components/Loading';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'react-i18next';
import { usePluginStore } from '@/web/core/plugin/store/plugin';
type Props = { pluginId: string };
const Render = ({ pluginId }: Props) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const { nodes = [] } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginModuleTemplates } = usePluginStore();
const filterTemplates = useMemo(() => {
const copyTemplates: SystemModuleTemplateType = JSON.parse(
JSON.stringify(PluginModuleTemplates)
);
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1,
[FlowNodeTypeEnum.pluginInput]: 1,
[FlowNodeTypeEnum.pluginOutput]: 1
};
// filter some template
nodes.forEach((node) => {
if (node.type && filterType[node.type]) {
copyTemplates.forEach((item) => {
item.list.forEach((module, index) => {
if (module.flowType === node.type) {
item.list.splice(index, 1);
}
});
});
}
});
return copyTemplates;
}, [nodes]);
const { data } = useQuery(['getOnePlugin', pluginId], () => getOnePlugin(pluginId), {
onError: (error) => {
toast({
status: 'warning',
title: getErrText(error, t('plugin.Load Plugin Failed'))
});
router.replace('/plugin/list');
}
});
useQuery(['getUserPlugs2ModuleTemplates'], () => loadPluginModuleTemplates());
const filterPlugins = useMemo(
() => pluginModuleTemplates.filter((item) => item.id !== pluginId),
[pluginId, pluginModuleTemplates]
);
return data ? (
<Flow
systemTemplates={filterTemplates}
pluginTemplates={[{ label: '', list: filterPlugins }]}
modules={data?.modules || []}
Header={<Header plugin={data} onClose={() => router.back()} />}
/>
) : (
<Loading />
);
};
export default function AdEdit(props: any) {
return (
<FlowProvider filterAppIds={[]}>
<Render {...props} />
</FlowProvider>
);
}
export async function getServerSideProps(context: any) {
return {
props: {
pluginId: context?.query?.pluginId || '',
...(await serviceSideProps(context))
}
};
}

View File

@@ -0,0 +1,212 @@
import React, { useCallback, useState } from 'react';
import {
Box,
Flex,
Button,
ModalHeader,
ModalBody,
Input,
Textarea,
IconButton
} from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImg } from '@/web/common/file/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@/web/common/hooks/useToast';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest } from '@/web/common/hooks/useRequest';
import { delOnePlugin, postCreatePlugin, putUpdatePlugin } from '@/web/core/plugin/api';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import MyIcon from '@/components/Icon';
export type FormType = {
id?: string;
avatar: string;
name: string;
intro: string;
};
export const defaultForm = {
avatar: '/icon/logo.svg',
name: '',
intro: ''
};
const CreateModal = ({
defaultValue = defaultForm,
onClose,
onSuccess,
onDelete
}: {
defaultValue?: FormType;
onClose: () => void;
onSuccess: () => void;
onDelete: () => void;
}) => {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const router = useRouter();
const { isPc } = useSystemStore();
const { openConfirm, ConfirmModal } = useConfirm({
title: t('common.Delete Tip'),
content: t('plugin.Confirm Delete')
});
const { register, setValue, getValues, handleSubmit } = useForm<FormType>({
defaultValues: defaultValue
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png,.svg',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, t('common.Select File Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (data: FormType) => {
return postCreatePlugin(data);
},
onSuccess(id: string) {
router.push(`/plugin/edit?pluginId=${id}`);
onSuccess();
onClose();
},
successToast: t('common.Create Success'),
errorToast: t('common.Create Failed')
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: async (data: FormType) => {
if (!data.id) return Promise.resolve('');
// @ts-ignore
return putUpdatePlugin(data);
},
onSuccess() {
onSuccess();
onClose();
},
successToast: t('common.Update Success'),
errorToast: t('common.Update Failed')
});
const onclickDelApp = useCallback(async () => {
if (!defaultValue.id) return;
try {
await delOnePlugin(defaultValue.id);
toast({
title: t('common.Delete Success'),
status: 'success'
});
onDelete();
} catch (err: any) {
toast({
title: getErrText(err, t('common.Delete Failed')),
status: 'error'
});
}
onClose();
}, [defaultValue.id, onClose, toast, t, onDelete]);
return (
<MyModal isOpen onClose={onClose} isCentered={!isPc}>
<ModalHeader fontSize={'2xl'}>
{defaultValue.id ? t('plugin.Update Your Plugin') : t('plugin.Create Your Plugin')}
</ModalHeader>
<ModalBody>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('plugin.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('avatar')}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
autoFocus={!defaultValue.id}
bg={'myWhite.600'}
{...register('name', {
required: t("common.Name Can't Be Empty")
})}
/>
</Flex>
<Box mt={3}>
<Box mb={1}>{t('plugin.Intro')}</Box>
<Textarea {...register('intro')} bg={'myWhite.600'} rows={5} />
</Box>
</ModalBody>
<Flex px={5} py={4}>
{!!defaultValue.id && (
<IconButton
className="delete"
size={'sm'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
_hover={{
bg: 'red.100'
}}
onClick={(e) => {
e.stopPropagation();
openConfirm(onclickDelApp)();
}}
/>
)}
<Box flex={1} />
<Button variant={'base'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
{!!defaultValue.id ? (
<Button isLoading={updating} onClick={handleSubmit((data) => onclickUpdate(data))}>
{t('common.Confirm Update')}
</Button>
) : (
<Button isLoading={creating} onClick={handleSubmit((data) => onclickCreate(data))}>
{t('common.Confirm Create')}
</Button>
)}
</Flex>
<File onSelect={onSelectFile} />
<ConfirmModal />
</MyModal>
);
};
export default CreateModal;

View File

@@ -0,0 +1,137 @@
import React, { useState } from 'react';
import { Box, Grid, Card, useTheme, Flex, IconButton, Button, Image } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
import { AddIcon } from '@chakra-ui/icons';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import MyIcon from '@/components/Icon';
import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
import EditModal, { defaultForm, FormType } from './component/EditModal';
import { getUserPlugins } from '@/web/core/plugin/api';
import EmptyTip from '@/components/EmptyTip';
const MyModules = () => {
const { t } = useTranslation();
const theme = useTheme();
const router = useRouter();
const [editModalData, setEditModalData] = useState<FormType>();
/* load plugins */
const {
data = [],
isLoading,
refetch
} = useQuery(['loadModules'], () => getUserPlugins(), {
refetchOnMount: true
});
return (
<PageContainer isLoading={isLoading}>
<Flex pt={3} px={5} alignItems={'center'}>
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/plugin.svg'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={['20px', '24px']} fontWeight={'bold'}>
{t('plugin.My Plugins')}({t('common.Beta')})
</Box>
</Flex>
<Button
leftIcon={<AddIcon />}
variant={'base'}
onClick={() => setEditModalData(defaultForm)}
>
{t('common.New Create')}
</Button>
</Flex>
<Grid
p={5}
gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']}
gridGap={5}
>
{data.map((plugin) => (
<Card
key={plugin._id}
py={4}
px={5}
cursor={'pointer'}
h={'140px'}
border={theme.borders.md}
boxShadow={'none'}
userSelect={'none'}
position={'relative'}
_hover={{
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
borderColor: 'transparent',
'& .delete': {
display: 'block'
},
'& .chat': {
display: 'block'
}
}}
onClick={() => router.push(`/plugin/edit?pluginId=${plugin._id}`)}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={plugin.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3}>{plugin.name}</Box>
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'sm'}
icon={<MyIcon name={'edit'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
display={['', 'none']}
_hover={{
bg: 'myBlue.200'
}}
onClick={(e) => {
e.stopPropagation();
setEditModalData({
id: plugin._id,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro
});
}}
/>
</Flex>
<Box
className={'textEllipsis3'}
py={2}
wordBreak={'break-all'}
fontSize={'sm'}
color={'myGray.600'}
>
{plugin.intro || t('plugin.No Intro')}
</Box>
</Card>
))}
</Grid>
{data.length === 0 && <EmptyTip />}
{!!editModalData && (
<EditModal
defaultValue={editModalData}
onClose={() => setEditModalData(undefined)}
onSuccess={refetch}
onDelete={refetch}
/>
)}
</PageContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content))
}
};
}
export default MyModules;

View File

@@ -25,6 +25,11 @@ const Tools = () => {
}
]
: []),
{
icon: 'common/navbar/pluginLight',
label: '自定义模块',
link: '/plugin/list'
},
...(feConfigs?.show_git
? [
{