feat: v4
This commit is contained in:
@@ -4,9 +4,9 @@ import { connectToDatabase, Chat, Model } from '@/service/mongo';
|
||||
import type { InitChatResponse } from '@/api/response/chat';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
import mongoose from 'mongoose';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -21,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
|
||||
// 没有 modelId 时,直接获取用户的第一个id
|
||||
const model = await (async () => {
|
||||
const app = await (async () => {
|
||||
if (!modelId) {
|
||||
const myModel = await Model.findOne({ userId });
|
||||
if (!myModel) {
|
||||
@@ -29,23 +29,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
name: '应用1',
|
||||
userId
|
||||
});
|
||||
return (await Model.findById(_id)) as ModelSchema;
|
||||
return (await Model.findById(_id)) as AppSchema;
|
||||
} else {
|
||||
return myModel;
|
||||
}
|
||||
} else {
|
||||
// 校验使用权限
|
||||
const authRes = await authModel({
|
||||
modelId,
|
||||
const authRes = await authApp({
|
||||
appId: modelId,
|
||||
userId,
|
||||
authUser: false,
|
||||
authOwner: false
|
||||
});
|
||||
return authRes.model;
|
||||
return authRes.app;
|
||||
}
|
||||
})();
|
||||
|
||||
modelId = modelId || model._id;
|
||||
modelId = modelId || app._id;
|
||||
|
||||
// 历史记录
|
||||
let history: ChatItemType[] = [];
|
||||
@@ -87,21 +87,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
]);
|
||||
}
|
||||
|
||||
const isOwner = String(model.userId) === userId;
|
||||
const isOwner = String(app.userId) === userId;
|
||||
|
||||
jsonRes<InitChatResponse>(res, {
|
||||
data: {
|
||||
chatId: chatId || '',
|
||||
modelId: modelId,
|
||||
model: {
|
||||
name: model.name,
|
||||
avatar: model.avatar,
|
||||
intro: model.intro,
|
||||
canUse: model.share.isShare || isOwner
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro,
|
||||
canUse: app.share.isShare || isOwner
|
||||
},
|
||||
chatModel: model.chat.chatModel,
|
||||
systemPrompt: isOwner ? model.chat.systemPrompt : '',
|
||||
limitPrompt: isOwner ? model.chat.limitPrompt : '',
|
||||
chatModel: app.chat.chatModel,
|
||||
systemPrompt: isOwner ? app.chat.systemPrompt : '',
|
||||
limitPrompt: isOwner ? app.chat.limitPrompt : '',
|
||||
history
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { connectToDatabase, Chat, Model } from '@/service/mongo';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { Types } from 'mongoose';
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function saveChat({
|
||||
userId
|
||||
}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> {
|
||||
await connectToDatabase();
|
||||
const { model } = await authModel({ modelId, userId, authOwner: false });
|
||||
const { app } = await authApp({ appId: modelId, userId, authOwner: false });
|
||||
|
||||
const content = prompts.map((item) => ({
|
||||
_id: item._id,
|
||||
@@ -59,7 +59,7 @@ export async function saveChat({
|
||||
quote: item.quote || []
|
||||
}));
|
||||
|
||||
if (String(model.userId) === userId) {
|
||||
if (String(app.userId) === userId) {
|
||||
await Model.findByIdAndUpdate(modelId, {
|
||||
updateTime: new Date()
|
||||
});
|
||||
@@ -93,8 +93,8 @@ export async function saveChat({
|
||||
newChatId: String(res._id)
|
||||
}))
|
||||
]),
|
||||
// update model
|
||||
...(String(model.userId) === userId
|
||||
// update app
|
||||
...(String(app.userId) === userId
|
||||
? [
|
||||
Model.findByIdAndUpdate(modelId, {
|
||||
updateTime: new Date()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ShareChat } from '@/service/mongo';
|
||||
import { authModel, authUser } from '@/service/utils/auth';
|
||||
import { authApp, authUser } from '@/service/utils/auth';
|
||||
import type { ShareChatEditType } from '@/types/model';
|
||||
|
||||
/* create a shareChat */
|
||||
@@ -14,8 +14,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
await authModel({
|
||||
modelId,
|
||||
await authApp({
|
||||
appId: modelId,
|
||||
userId,
|
||||
authOwner: false
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ShareChat, User } from '@/service/mongo';
|
||||
import type { InitShareChatResponse } from '@/api/response/chat';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
import { hashPassword } from '@/service/utils/tools';
|
||||
import { HUMAN_ICON } from '@/constants/chat';
|
||||
|
||||
@@ -35,8 +35,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// 校验使用权限
|
||||
const { model } = await authModel({
|
||||
modelId: shareChat.modelId,
|
||||
const { app } = await authApp({
|
||||
appId: shareChat.modelId,
|
||||
userId: String(shareChat.userId),
|
||||
authOwner: false
|
||||
});
|
||||
@@ -48,11 +48,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
maxContext: shareChat.maxContext,
|
||||
userAvatar: user?.avatar || HUMAN_ICON,
|
||||
model: {
|
||||
name: model.name,
|
||||
avatar: model.avatar,
|
||||
intro: model.intro
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro
|
||||
},
|
||||
chatModel: model.chat.chatModel
|
||||
chatModel: app.chat.chatModel
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { Chat, Model, connectToDatabase, Collection, ShareChat } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -19,8 +19,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
await connectToDatabase();
|
||||
|
||||
// 验证是否是该用户的 model
|
||||
await authModel({
|
||||
modelId,
|
||||
await authApp({
|
||||
appId: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -18,14 +18,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { model } = await authModel({
|
||||
modelId,
|
||||
const { app } = await authApp({
|
||||
appId: modelId,
|
||||
userId,
|
||||
authOwner: false
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: model
|
||||
data: app
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
await connectToDatabase();
|
||||
|
||||
// 根据 userId 获取模型信息
|
||||
const [myModels, myCollections] = await Promise.all([
|
||||
const [myApps, myCollections] = await Promise.all([
|
||||
Model.find(
|
||||
{
|
||||
userId
|
||||
@@ -33,20 +33,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
jsonRes<ModelListResponse>(res, {
|
||||
data: {
|
||||
myModels: myModels.map((item) => ({
|
||||
myApps: myApps.map((item) => ({
|
||||
_id: item._id,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
intro: item.intro
|
||||
})),
|
||||
myCollectionModels: myCollections
|
||||
myCollectionApps: myCollections
|
||||
.map((item: any) => ({
|
||||
_id: item.modelId?._id,
|
||||
name: item.modelId?.name,
|
||||
avatar: item.modelId?.avatar,
|
||||
intro: item.modelId?.intro
|
||||
}))
|
||||
.filter((item) => !myModels.find((model) => String(model._id) === String(item._id))) // 去重
|
||||
.filter((item) => !myApps.find((model) => String(model._id) === String(item._id))) // 去重
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,16 +4,16 @@ import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { Model } from '@/service/models/model';
|
||||
import type { ModelUpdateParams } from '@/types/model';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { name, avatar, chat, share, intro } = req.body as ModelUpdateParams;
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
const { name, avatar, chat, share, intro, modules } = req.body as ModelUpdateParams;
|
||||
const { appId } = req.query as { appId: string };
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('参数错误');
|
||||
if (!appId) {
|
||||
throw new Error('appId is empty');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
@@ -21,15 +21,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await authModel({
|
||||
modelId,
|
||||
await authApp({
|
||||
appId,
|
||||
userId
|
||||
});
|
||||
|
||||
// 更新模型
|
||||
await Model.updateOne(
|
||||
{
|
||||
_id: modelId,
|
||||
_id: appId,
|
||||
userId
|
||||
},
|
||||
{
|
||||
@@ -40,7 +40,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
...(share && {
|
||||
'share.isShare': share.isShare,
|
||||
'share.isShareDetail': share.isShareDetail
|
||||
})
|
||||
}),
|
||||
...(modules && { modules })
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser, authModel, getApiKey } from '@/service/utils/auth';
|
||||
import { authUser, authApp, getApiKey } from '@/service/utils/auth';
|
||||
import { modelServiceToolMap, resStreamResponse } from '@/service/utils/chat';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
@@ -50,19 +50,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
/* 凭证校验 */
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
const { model } = await authModel({
|
||||
const { app } = await authApp({
|
||||
userId,
|
||||
modelId
|
||||
appId: modelId
|
||||
});
|
||||
|
||||
/* get api key */
|
||||
const { systemAuthKey: apiKey } = await getApiKey({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
userId,
|
||||
mustPay: true
|
||||
});
|
||||
|
||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
||||
const modelConstantsData = ChatModelMap[app.chat.chatModel];
|
||||
const prompt = prompts[prompts.length - 1];
|
||||
|
||||
const {
|
||||
@@ -71,14 +71,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
quotePrompt = []
|
||||
} = await (async () => {
|
||||
// 使用了知识库搜索
|
||||
if (model.chat.relatedKbs?.length > 0) {
|
||||
if (app.chat.relatedKbs?.length > 0) {
|
||||
const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
||||
model,
|
||||
model: app,
|
||||
userId,
|
||||
fixedQuote: [],
|
||||
prompt: prompt,
|
||||
similarity: model.chat.searchSimilarity,
|
||||
limit: model.chat.searchLimit
|
||||
similarity: app.chat.searchSimilarity,
|
||||
limit: app.chat.searchLimit
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -88,19 +88,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
};
|
||||
}
|
||||
return {
|
||||
userSystemPrompt: model.chat.systemPrompt
|
||||
userSystemPrompt: app.chat.systemPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: model.chat.systemPrompt
|
||||
value: app.chat.systemPrompt
|
||||
}
|
||||
]
|
||||
: [],
|
||||
userLimitPrompt: model.chat.limitPrompt
|
||||
userLimitPrompt: app.chat.limitPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: model.chat.limitPrompt
|
||||
value: app.chat.limitPrompt
|
||||
}
|
||||
]
|
||||
: []
|
||||
@@ -108,8 +108,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
})();
|
||||
|
||||
// search result is empty
|
||||
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
|
||||
const response = model.chat.searchEmptyText;
|
||||
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
|
||||
const response = app.chat.searchEmptyText;
|
||||
return res.end(response);
|
||||
}
|
||||
|
||||
@@ -123,14 +123,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
];
|
||||
|
||||
// 计算温度
|
||||
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
|
||||
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
|
||||
2
|
||||
);
|
||||
|
||||
// 发出请求
|
||||
const { streamResponse, responseMessages, responseText, totalTokens } =
|
||||
await modelServiceToolMap.chatCompletion({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
apiKey,
|
||||
temperature: +temperature,
|
||||
messages: completePrompts,
|
||||
@@ -146,7 +146,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
if (isStream) {
|
||||
try {
|
||||
const { finishMessages, totalTokens } = await resStreamResponse({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
res,
|
||||
chatResponse: streamResponse,
|
||||
prompts: responseMessages
|
||||
@@ -173,7 +173,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
chatModel: model.chat.chatModel,
|
||||
chatModel: app.chat.chatModel,
|
||||
userId,
|
||||
textLen,
|
||||
tokens,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import { authApp } from '@/service/utils/auth';
|
||||
import { ChatModelMap } from '@/constants/model';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { openaiEmbedding } from '../plugin/openaiEmbedding';
|
||||
@@ -54,13 +54,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
}
|
||||
|
||||
// auth model
|
||||
const { model } = await authModel({
|
||||
modelId: appId,
|
||||
const { app } = await authApp({
|
||||
appId,
|
||||
userId
|
||||
});
|
||||
|
||||
const result = await appKbSearch({
|
||||
model,
|
||||
model: app,
|
||||
userId,
|
||||
fixedQuote: [],
|
||||
prompt: prompts[prompts.length - 1],
|
||||
@@ -88,7 +88,7 @@ export async function appKbSearch({
|
||||
similarity = 0.8,
|
||||
limit = 5
|
||||
}: {
|
||||
model: ModelSchema;
|
||||
model: AppSchema;
|
||||
userId: string;
|
||||
fixedQuote?: QuoteItemType[];
|
||||
prompt: ChatItemType;
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function classifyQuestion({
|
||||
if (!arg.type) {
|
||||
throw new Error('');
|
||||
}
|
||||
console.log(adaptMessages, arg.type);
|
||||
console.log(arg.type);
|
||||
|
||||
return {
|
||||
[arg.type]: 1
|
||||
|
||||
@@ -5,9 +5,8 @@ import { sseResponse } from '@/service/utils/tools';
|
||||
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { ChatCompletionType, ChatContextFilter } from '@/service/utils/chat/index';
|
||||
import { ChatContextFilter } from '@/service/utils/chat/index';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import { getSystemOpenAiKey } from '@/service/utils/auth';
|
||||
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
import { parseStreamChunk, textAdaptGptResponse } from '@/utils/adapt';
|
||||
import { getOpenAIApi, axiosConfig } from '@/service/ai/openai';
|
||||
@@ -23,7 +22,7 @@ export type Props = {
|
||||
systemPrompt?: string;
|
||||
limitPrompt?: string;
|
||||
};
|
||||
export type Response = { history: ChatItemType[] };
|
||||
export type Response = { answer: string };
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
@@ -89,7 +88,7 @@ export async function chatCompletion({
|
||||
userChatInput,
|
||||
systemPrompt,
|
||||
limitPrompt
|
||||
}: Props & { res: NextApiResponse }) {
|
||||
}: Props & { res: NextApiResponse }): Promise<Response> {
|
||||
const messages: ChatItemType[] = [
|
||||
...(quotePrompt
|
||||
? [
|
||||
@@ -131,7 +130,6 @@ export async function chatCompletion({
|
||||
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||
const chatAPI = getOpenAIApi();
|
||||
console.log(adaptMessages);
|
||||
|
||||
/* count response max token */
|
||||
const promptsToken = modelToolMap[model].countTokens({
|
||||
@@ -156,37 +154,35 @@ export async function chatCompletion({
|
||||
}
|
||||
);
|
||||
|
||||
const { answer, totalTokens } = await (async () => {
|
||||
const { answer } = await (async () => {
|
||||
if (stream) {
|
||||
// sse response
|
||||
const { answer } = await streamResponse({ res, response });
|
||||
// count tokens
|
||||
const finishMessages = filterMessages.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: answer
|
||||
});
|
||||
// const finishMessages = filterMessages.concat({
|
||||
// obj: ChatRoleEnum.AI,
|
||||
// value: answer
|
||||
// });
|
||||
|
||||
const totalTokens = modelToolMap[model].countTokens({
|
||||
messages: finishMessages
|
||||
});
|
||||
// const totalTokens = modelToolMap[model].countTokens({
|
||||
// messages: finishMessages
|
||||
// });
|
||||
|
||||
return {
|
||||
answer,
|
||||
totalTokens
|
||||
answer
|
||||
// totalTokens
|
||||
};
|
||||
} else {
|
||||
const answer = stream ? '' : response.data.choices?.[0].message?.content || '';
|
||||
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
||||
// const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
||||
|
||||
return {
|
||||
answer,
|
||||
totalTokens
|
||||
answer
|
||||
// totalTokens
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
// count price
|
||||
const unitPrice = ChatModelMap[model]?.price || 3;
|
||||
return {
|
||||
answer
|
||||
};
|
||||
|
||||
20
client/src/pages/api/openapi/modules/init/history.tsx
Normal file
20
client/src/pages/api/openapi/modules/init/history.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
|
||||
export type Props = {
|
||||
maxContext: number;
|
||||
[SystemInputEnum.history]: ChatItemType[];
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { maxContext = 5, history } = req.body as Props;
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
history: history.slice(-maxContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
17
client/src/pages/api/openapi/modules/init/userChatInput.tsx
Normal file
17
client/src/pages/api/openapi/modules/init/userChatInput.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
export type Props = {
|
||||
[SystemInputEnum.userChatInput]: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { userChatInput } = req.body as Props;
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
userChatInput
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -25,7 +25,7 @@ type Props = {
|
||||
type Response = {
|
||||
rawSearch: QuoteItemType[];
|
||||
isEmpty?: boolean;
|
||||
quotePrompt: string;
|
||||
quotePrompt?: string;
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -43,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
throw new Error('params is error');
|
||||
}
|
||||
|
||||
const result = await appKbSearch({
|
||||
const result = await kbSearch({
|
||||
kb_ids,
|
||||
history,
|
||||
similarity,
|
||||
@@ -64,7 +64,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
}
|
||||
});
|
||||
|
||||
export async function appKbSearch({
|
||||
export async function kbSearch({
|
||||
kb_ids = [],
|
||||
history = [],
|
||||
similarity = 0.8,
|
||||
@@ -108,8 +108,8 @@ export async function appKbSearch({
|
||||
const rawSearch = searchRes.slice(0, sliceResult.length);
|
||||
|
||||
return {
|
||||
isEmpty: rawSearch.length === 0,
|
||||
isEmpty: rawSearch.length === 0 ? true : undefined,
|
||||
rawSearch,
|
||||
quotePrompt: sliceResult ? `知识库:\n${sliceResult}` : ''
|
||||
quotePrompt: sliceResult ? `知识库:\n${sliceResult}` : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export type Props = {
|
||||
url: string;
|
||||
body: Record<string, any>;
|
||||
};
|
||||
@@ -96,7 +96,7 @@ export async function openaiEmbedding_system({ input }: Props) {
|
||||
input
|
||||
},
|
||||
{
|
||||
timeout: 60000,
|
||||
timeout: 20000,
|
||||
...axiosConfig(apiKey)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatModelMap } from '@/constants/model';
|
||||
@@ -79,9 +79,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
}
|
||||
|
||||
// auth app permission
|
||||
const { model, showModelDetail } = await authModel({
|
||||
const { app, showModelDetail } = await authApp({
|
||||
userId,
|
||||
modelId: appId,
|
||||
appId,
|
||||
authOwner: false,
|
||||
reserveDetail: true
|
||||
});
|
||||
@@ -90,7 +90,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
|
||||
/* get api key */
|
||||
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
userId,
|
||||
mustPay: authType !== 'token'
|
||||
});
|
||||
@@ -112,14 +112,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
quotePrompt = []
|
||||
} = await (async () => {
|
||||
// 使用了知识库搜索
|
||||
if (model.chat.relatedKbs?.length > 0) {
|
||||
if (app.chat.relatedKbs?.length > 0) {
|
||||
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
||||
model,
|
||||
model: app,
|
||||
userId,
|
||||
fixedQuote: history[history.length - 1]?.quote,
|
||||
prompt,
|
||||
similarity: model.chat.searchSimilarity,
|
||||
limit: model.chat.searchLimit
|
||||
similarity: app.chat.searchSimilarity,
|
||||
limit: app.chat.searchLimit
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -130,19 +130,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
};
|
||||
}
|
||||
return {
|
||||
userSystemPrompt: model.chat.systemPrompt
|
||||
userSystemPrompt: app.chat.systemPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: model.chat.systemPrompt
|
||||
value: app.chat.systemPrompt
|
||||
}
|
||||
]
|
||||
: [],
|
||||
userLimitPrompt: model.chat.limitPrompt
|
||||
userLimitPrompt: app.chat.limitPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: model.chat.limitPrompt
|
||||
value: app.chat.limitPrompt
|
||||
}
|
||||
]
|
||||
: []
|
||||
@@ -150,15 +150,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
})();
|
||||
|
||||
// search result is empty
|
||||
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
|
||||
const response = model.chat.searchEmptyText;
|
||||
if (app.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && app.chat.searchEmptyText) {
|
||||
const response = app.chat.searchEmptyText;
|
||||
if (stream) {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text: response,
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
finish_reason: 'stop'
|
||||
})
|
||||
});
|
||||
@@ -166,9 +166,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
} else {
|
||||
return res.json({
|
||||
id: chatId || '',
|
||||
model: model.chat.chatModel,
|
||||
object: 'chat.completion',
|
||||
created: 1688608930,
|
||||
model: app.chat.chatModel,
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
||||
choices: [
|
||||
{ message: { role: 'assistant', content: response }, finish_reason: 'stop', index: 0 }
|
||||
@@ -186,9 +186,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
prompt
|
||||
];
|
||||
// chat temperature
|
||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
||||
const modelConstantsData = ChatModelMap[app.chat.chatModel];
|
||||
// FastGpt temperature range: 1~10
|
||||
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
|
||||
const temperature = (modelConstantsData.maxTemperature * (app.chat.temperature / 10)).toFixed(
|
||||
2
|
||||
);
|
||||
|
||||
@@ -196,13 +196,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
|
||||
});
|
||||
|
||||
// start model api. responseText and totalTokens: valid only if stream = false
|
||||
// start app api. responseText and totalTokens: valid only if stream = false
|
||||
const { streamResponse, responseMessages, responseText, totalTokens } =
|
||||
await modelServiceToolMap.chatCompletion({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
apiKey: userOpenAiKey || apiKey,
|
||||
temperature: +temperature,
|
||||
maxToken: model.chat.maxToken,
|
||||
maxToken: app.chat.maxToken,
|
||||
messages: completePrompts,
|
||||
stream,
|
||||
res
|
||||
@@ -242,7 +242,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
});
|
||||
// response answer
|
||||
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
res,
|
||||
chatResponse: streamResponse,
|
||||
prompts: responseMessages
|
||||
@@ -300,7 +300,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
id: chatId || '',
|
||||
object: 'chat.completion',
|
||||
created: 1688608930,
|
||||
model: model.chat.chatModel,
|
||||
model: app.chat.chatModel,
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
|
||||
choices: [
|
||||
{ message: { role: 'assistant', content: answer }, finish_reason: 'stop', index: 0 }
|
||||
@@ -310,7 +310,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
|
||||
pushChatBill({
|
||||
isPay: !userOpenAiKey,
|
||||
chatModel: model.chat.chatModel,
|
||||
chatModel: app.chat.chatModel,
|
||||
userId,
|
||||
textLen,
|
||||
tokens,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||
import { authUser, authApp, getApiKey, authShareChat } from '@/service/utils/auth';
|
||||
import { sseErrRes, jsonRes } from '@/service/response';
|
||||
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
@@ -13,14 +13,14 @@ import { type ChatCompletionRequestMessage } from 'openai';
|
||||
import {
|
||||
kbChatAppDemo,
|
||||
chatAppDemo,
|
||||
lafClassifyQuestionDemo,
|
||||
classifyQuestionDemo,
|
||||
SpecificInputEnum,
|
||||
AppModuleItemTypeEnum
|
||||
} from '@/constants/app';
|
||||
import { Types } from 'mongoose';
|
||||
import { model, Types } from 'mongoose';
|
||||
import { moduleFetch } from '@/service/api/request';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { AppModuleItemType, RunningModuleItemType } from '@/types/app';
|
||||
import { FlowInputItemTypeEnum, FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
|
||||
type FastGptWebChatProps = {
|
||||
@@ -82,8 +82,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
throw new Error('appId is empty');
|
||||
}
|
||||
|
||||
// get history
|
||||
const { history } = await getChatHistory({ chatId, userId });
|
||||
// auth app, get history
|
||||
const [{ app }, { history }] = await Promise.all([
|
||||
authApp({
|
||||
appId,
|
||||
userId
|
||||
}),
|
||||
getChatHistory({ chatId, userId })
|
||||
]);
|
||||
|
||||
const prompts = history.concat(gptMessage2ChatType(messages));
|
||||
if (prompts[prompts.length - 1].obj === 'AI') {
|
||||
prompts.pop();
|
||||
@@ -95,12 +102,15 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
throw new Error('Question is empty');
|
||||
}
|
||||
|
||||
/* start process */
|
||||
const modules = JSON.parse(JSON.stringify(classifyQuestionDemo.modules));
|
||||
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
|
||||
if (stream && newChatId) {
|
||||
res.setHeader('newChatId', String(newChatId));
|
||||
}
|
||||
|
||||
/* start process */
|
||||
const { responseData, answerText } = await dispatchModules({
|
||||
res,
|
||||
modules,
|
||||
modules: app.modules,
|
||||
params: {
|
||||
history: prompts,
|
||||
userChatInput: prompt.value
|
||||
@@ -110,8 +120,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
|
||||
// save chat
|
||||
if (typeof chatId === 'string') {
|
||||
const { newChatId } = await saveChat({
|
||||
await saveChat({
|
||||
chatId,
|
||||
newChatId,
|
||||
modelId: appId,
|
||||
prompts: [
|
||||
prompt,
|
||||
@@ -124,19 +135,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
],
|
||||
userId
|
||||
});
|
||||
|
||||
if (newChatId) {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.chatResponse,
|
||||
data: JSON.stringify({
|
||||
newChatId
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: '[DONE]'
|
||||
});
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.appStreamResponse,
|
||||
@@ -145,7 +151,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
res.end();
|
||||
} else {
|
||||
res.json({
|
||||
data: responseData,
|
||||
data: {
|
||||
newChatId,
|
||||
...responseData
|
||||
},
|
||||
id: chatId || '',
|
||||
model: '',
|
||||
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
||||
@@ -183,6 +192,7 @@ async function dispatchModules({
|
||||
params?: Record<string, any>;
|
||||
stream?: boolean;
|
||||
}) {
|
||||
const runningModules = loadModules(modules);
|
||||
let storeData: Record<string, any> = {};
|
||||
let responseData: Record<string, any> = {};
|
||||
let answerText = '';
|
||||
@@ -212,7 +222,10 @@ async function dispatchModules({
|
||||
...data
|
||||
};
|
||||
}
|
||||
function moduleInput(module: AppModuleItemType, data: Record<string, any> = {}): Promise<any> {
|
||||
function moduleInput(
|
||||
module: RunningModuleItemType,
|
||||
data: Record<string, any> = {}
|
||||
): Promise<any> {
|
||||
const checkInputFinish = () => {
|
||||
return !module.inputs.find((item: any) => item.value === undefined);
|
||||
};
|
||||
@@ -222,50 +235,58 @@ async function dispatchModules({
|
||||
module.inputs[index].value = value;
|
||||
};
|
||||
|
||||
const set = new Set();
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(data).map(([key, val]: any) => {
|
||||
updateInputValue(key, val);
|
||||
if (checkInputFinish()) {
|
||||
|
||||
if (!set.has(module.moduleId) && checkInputFinish()) {
|
||||
set.add(module.moduleId);
|
||||
return moduleRun(module);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
function moduleOutput(module: AppModuleItemType, result: Record<string, any> = {}): Promise<any> {
|
||||
function moduleOutput(
|
||||
module: RunningModuleItemType,
|
||||
result: Record<string, any> = {}
|
||||
): Promise<any> {
|
||||
return Promise.all(
|
||||
module.outputs.map((item) => {
|
||||
if (result[item.key] === undefined) return;
|
||||
module.outputs.map((outputItem) => {
|
||||
if (result[outputItem.key] === undefined) return;
|
||||
/* update output value */
|
||||
item.value = result[item.key];
|
||||
outputItem.value = result[outputItem.key];
|
||||
|
||||
pushStore({
|
||||
isResponse: item.response,
|
||||
answer: item.answer ? item.value : '',
|
||||
isResponse: outputItem.response,
|
||||
answer: outputItem.answer ? outputItem.value : '',
|
||||
data: {
|
||||
[item.key]: item.value
|
||||
[outputItem.key]: outputItem.value
|
||||
}
|
||||
});
|
||||
|
||||
/* update target */
|
||||
return Promise.all(
|
||||
item.targets.map((target: any) => {
|
||||
outputItem.targets.map((target: any) => {
|
||||
// find module
|
||||
const targetModule = modules.find((item) => item.moduleId === target.moduleId);
|
||||
const targetModule = runningModules.find((item) => item.moduleId === target.moduleId);
|
||||
if (!targetModule) return;
|
||||
return moduleInput(targetModule, { [target.key]: item.value });
|
||||
return moduleInput(targetModule, { [target.key]: outputItem.value });
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
async function moduleRun(module: AppModuleItemType): Promise<any> {
|
||||
async function moduleRun(module: RunningModuleItemType): Promise<any> {
|
||||
if (res.closed) return Promise.resolve();
|
||||
console.log('run=========', module.type, module.url);
|
||||
|
||||
if (module.type === AppModuleItemTypeEnum.answer) {
|
||||
pushStore({
|
||||
answer: module.inputs[0].value
|
||||
answer: module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value || ''
|
||||
});
|
||||
return AnswerResponse({
|
||||
return StreamAnswer({
|
||||
res,
|
||||
stream,
|
||||
text: module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value
|
||||
@@ -276,16 +297,19 @@ async function dispatchModules({
|
||||
return moduleOutput(module, switchResponse(module));
|
||||
}
|
||||
|
||||
if (module.type === AppModuleItemTypeEnum.http && module.url) {
|
||||
if (
|
||||
(module.type === AppModuleItemTypeEnum.http ||
|
||||
module.type === AppModuleItemTypeEnum.initInput) &&
|
||||
module.url
|
||||
) {
|
||||
// get fetch params
|
||||
const inputParams: Record<string, any> = {};
|
||||
const params: Record<string, any> = {};
|
||||
module.inputs.forEach((item: any) => {
|
||||
inputParams[item.key] = item.value;
|
||||
params[item.key] = item.value;
|
||||
});
|
||||
const data = {
|
||||
stream,
|
||||
...module.body,
|
||||
...inputParams
|
||||
...params
|
||||
};
|
||||
|
||||
// response data
|
||||
@@ -299,8 +323,12 @@ async function dispatchModules({
|
||||
}
|
||||
}
|
||||
|
||||
// 从填充 params 开始进入递归
|
||||
await Promise.all(modules.map((module) => moduleInput(module, params)));
|
||||
// start process width initInput
|
||||
const initModules = runningModules.filter(
|
||||
(item) => item.type === AppModuleItemTypeEnum.initInput
|
||||
);
|
||||
|
||||
await Promise.all(initModules.map((module) => moduleInput(module, params)));
|
||||
|
||||
return {
|
||||
responseData,
|
||||
@@ -308,7 +336,29 @@ async function dispatchModules({
|
||||
};
|
||||
}
|
||||
|
||||
function AnswerResponse({
|
||||
function loadModules(modules: AppModuleItemType[]): RunningModuleItemType[] {
|
||||
return modules.map((module) => {
|
||||
return {
|
||||
moduleId: module.moduleId,
|
||||
type: module.type,
|
||||
url: module.url,
|
||||
inputs: module.inputs
|
||||
.filter((item) => item.type !== FlowInputItemTypeEnum.target || item.connected) // filter unconnected target input
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value
|
||||
})),
|
||||
outputs: module.outputs.map((item) => ({
|
||||
key: item.key,
|
||||
answer: item.type === FlowOutputItemTypeEnum.answer,
|
||||
response: item.response,
|
||||
value: undefined,
|
||||
targets: item.targets
|
||||
}))
|
||||
};
|
||||
});
|
||||
}
|
||||
function StreamAnswer({
|
||||
res,
|
||||
stream = false,
|
||||
text = ''
|
||||
@@ -322,13 +372,13 @@ function AnswerResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text
|
||||
text: text.replace(/\\n/g, '\n')
|
||||
})
|
||||
});
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function switchResponse(module: any) {
|
||||
function switchResponse(module: RunningModuleItemType) {
|
||||
const val = module?.inputs?.[0]?.value;
|
||||
|
||||
if (val) {
|
||||
85
client/src/pages/app/detail/components/API.tsx
Normal file
85
client/src/pages/app/detail/components/API.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Divider, Flex, useTheme, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const API = ({ modelId }: { modelId: string }) => {
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
|
||||
const {
|
||||
isOpen: isOpenAPIModal,
|
||||
onOpen: onOpenAPIModal,
|
||||
onClose: onCloseAPIModal
|
||||
} = useDisclosure();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl(`${location.origin}/api/openapi`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Box display={['none', 'flex']} px={5} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
AppId:
|
||||
<Box
|
||||
as={'span'}
|
||||
ml={2}
|
||||
fontWeight={'bold'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(modelId, '已复制 AppId')}
|
||||
>
|
||||
{modelId}
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex
|
||||
bg={'myWhite.600'}
|
||||
py={2}
|
||||
px={4}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(baseUrl, '已复制 API 地址')}
|
||||
>
|
||||
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
|
||||
API服务器
|
||||
</Box>
|
||||
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
|
||||
{baseUrl}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Button
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name={'apikey'} w={'16px'} color={''} />}
|
||||
variant={'base'}
|
||||
onClick={onOpenAPIModal}
|
||||
>
|
||||
API 秘钥
|
||||
</Button>
|
||||
</Box>
|
||||
<Divider mt={3} />
|
||||
<Box flex={1}>
|
||||
<Skeleton h="100%" isLoaded={isLoaded} fadeDuration={2}>
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
src="https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh"
|
||||
frameBorder="0"
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
onError={() => setIsLoaded(true)}
|
||||
/>
|
||||
</Skeleton>
|
||||
</Box>
|
||||
{isOpenAPIModal && <APIKeyModal onClose={onCloseAPIModal} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default API;
|
||||
394
client/src/pages/app/detail/components/Kb.tsx
Normal file
394
client/src/pages/app/detail/components/Kb.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Grid,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { putAppById } from '@/api/app';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MySlider from '@/components/Slider';
|
||||
|
||||
const Kb = ({ modelId }: { modelId: string }) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { appDetail, loadKbList, loadAppDetail } = useUserStore();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, reset, getValues, setValue } = useForm({
|
||||
defaultValues: {
|
||||
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||
searchLimit: appDetail.chat.searchLimit,
|
||||
searchEmptyText: appDetail.chat.searchEmptyText
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenEditParams,
|
||||
onOpen: onOpenEditParams,
|
||||
onClose: onCloseEditParams
|
||||
} = useDisclosure();
|
||||
|
||||
const onchangeKb = useCallback(
|
||||
async (
|
||||
data: {
|
||||
relatedKbs?: string[];
|
||||
searchSimilarity?: number;
|
||||
searchLimit?: number;
|
||||
searchEmptyText?: string;
|
||||
} = {}
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await putAppById(modelId, {
|
||||
chat: {
|
||||
...appDetail.chat,
|
||||
...data
|
||||
}
|
||||
});
|
||||
loadAppDetail(modelId, true);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setIsLoading, modelId, appDetail.chat, loadAppDetail, toast]
|
||||
);
|
||||
|
||||
// init kb select list
|
||||
const { isLoading, data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
|
||||
|
||||
return (
|
||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||
<Box fontWeight={'bold'}>关联的知识库({appDetail.chat?.relatedKbs.length})</Box>
|
||||
{(() => {
|
||||
const kbs =
|
||||
appDetail.chat?.relatedKbs
|
||||
?.map((id) => kbList.find((kb) => kb._id === id))
|
||||
.filter((item) => item) || [];
|
||||
return (
|
||||
<Grid
|
||||
mt={2}
|
||||
gridTemplateColumns={[
|
||||
'repeat(1,1fr)',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={[3, 4]}
|
||||
>
|
||||
<Card
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'myGray.100'}
|
||||
_hover={{
|
||||
bg: 'white',
|
||||
color: 'myBlue.800'
|
||||
}}
|
||||
onClick={() => {
|
||||
reset({
|
||||
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||
searchLimit: appDetail.chat.searchLimit,
|
||||
searchEmptyText: appDetail.chat.searchEmptyText
|
||||
});
|
||||
onOpenEditParams();
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
|
||||
<IconButton
|
||||
mr={2}
|
||||
size={'sm'}
|
||||
borderRadius={'lg'}
|
||||
icon={<MyIcon name={'edit'} w={'14px'} color={'myGray.600'} />}
|
||||
aria-label={''}
|
||||
variant={'base'}
|
||||
/>
|
||||
调整搜索参数
|
||||
</Flex>
|
||||
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
||||
相似度: {appDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
||||
{appDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
||||
{appDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'myGray.100'}
|
||||
_hover={{
|
||||
bg: 'white',
|
||||
color: 'myBlue.800'
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedIdList(
|
||||
appDetail.chat?.relatedKbs ? [...appDetail.chat?.relatedKbs] : []
|
||||
);
|
||||
onOpenKbSelect();
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
|
||||
<IconButton
|
||||
mr={2}
|
||||
size={'sm'}
|
||||
borderRadius={'lg'}
|
||||
icon={<AddIcon />}
|
||||
aria-label={''}
|
||||
variant={'base'}
|
||||
/>
|
||||
选择关联知识库
|
||||
</Flex>
|
||||
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
||||
关联知识库,让 AI 应用回答你的特有内容。
|
||||
</Flex>
|
||||
</Card>
|
||||
{kbs.map((item) =>
|
||||
item ? (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
_hover={{
|
||||
boxShadow: 'lg',
|
||||
'& .detailBtn': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .delete': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['26px', '32px', '38px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={3} alignItems={'flex-end'} justifyContent={'flex-end'} h={'30px'}>
|
||||
<Button
|
||||
mr={3}
|
||||
className="detailBtn"
|
||||
display={['flex', 'none']}
|
||||
variant={'base'}
|
||||
size={'sm'}
|
||||
onClick={() => router.push(`/kb?kbId=${item._id}`)}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
<IconButton
|
||||
className="delete"
|
||||
display={['flex', 'none']}
|
||||
icon={<DeleteIcon />}
|
||||
variant={'outline'}
|
||||
aria-label={'delete'}
|
||||
size={'sm'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
const ids = appDetail.chat?.relatedKbs
|
||||
? [...appDetail.chat.relatedKbs]
|
||||
: [];
|
||||
const i = ids.findIndex((id) => id === item._id);
|
||||
ids.splice(i, 1);
|
||||
onchangeKb({ relatedKbs: ids });
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : null
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
})()}
|
||||
{/* select kb modal */}
|
||||
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
w={'800px'}
|
||||
maxW={'90vw'}
|
||||
h={['90vh', 'auto']}
|
||||
>
|
||||
<ModalHeader>关联的知识库({selectedIdList.length})</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody
|
||||
flex={['1 0 0', '0 0 auto']}
|
||||
maxH={'80vh'}
|
||||
overflowY={'auto'}
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
|
||||
gridGap={3}
|
||||
>
|
||||
{kbList.map((item) => (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
order={appDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
{...(selectedIdList.includes(item._id)
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
let ids = [...selectedIdList];
|
||||
if (!selectedIdList.includes(item._id)) {
|
||||
ids = ids.concat(item._id);
|
||||
} else {
|
||||
const i = ids.findIndex((id) => id === item._id);
|
||||
ids.splice(i, 1);
|
||||
}
|
||||
|
||||
ids = ids.filter((id) => kbList.find((item) => item._id === id));
|
||||
setSelectedIdList(ids);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCloseKbSelect();
|
||||
onchangeKb({ relatedKbs: selectedIdList });
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* edit mode */}
|
||||
<Modal isOpen={isOpenEditParams} onClose={onCloseEditParams}>
|
||||
<ModalOverlay />
|
||||
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
|
||||
<ModalHeader>搜索参数调整</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex pt={3} pb={5}>
|
||||
<Box flex={'0 0 100px'}>
|
||||
相似度
|
||||
<Tooltip label={'高相似度推荐0.8及以上。'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</Tooltip>
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex py={8}>
|
||||
<Box flex={'0 0 100px'}>单次搜索数量</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>
|
||||
</Flex>
|
||||
<Flex pt={3}>
|
||||
<Box flex={'0 0 100px'}>空搜索回复</Box>
|
||||
<Box flex={1}>
|
||||
<Textarea
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
placeholder={
|
||||
'若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文,FastGpt 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。'
|
||||
}
|
||||
{...register('searchEmptyText')}
|
||||
></Textarea>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onCloseEditParams}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCloseEditParams();
|
||||
onchangeKb({
|
||||
searchSimilarity: getValues('searchSimilarity'),
|
||||
searchLimit: getValues('searchLimit'),
|
||||
searchEmptyText: getValues('searchEmptyText')
|
||||
});
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Kb;
|
||||
390
client/src/pages/app/detail/components/Settings.tsx
Normal file
390
client/src/pages/app/detail/components/Settings.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
Divider,
|
||||
Tooltip
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { delModelById, putAppById } from '@/api/app';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { ChatModelMap, getChatModelList } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
|
||||
const systemPromptTip =
|
||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。';
|
||||
const limitPromptTip =
|
||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"';
|
||||
|
||||
const Settings = ({ modelId }: { modelId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { userInfo, appDetail, myApps, loadAppDetail, refreshModel, setLastModelId } =
|
||||
useUserStore();
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认删除该应用?'
|
||||
});
|
||||
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
defaultValues: appDetail
|
||||
});
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => appDetail.userId === userInfo?._id,
|
||||
[appDetail.userId, userInfo?._id]
|
||||
);
|
||||
const tokenLimit = useMemo(() => {
|
||||
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
|
||||
|
||||
if (max < getValues('chat.maxToken')) {
|
||||
setValue('chat.maxToken', max);
|
||||
}
|
||||
|
||||
return max;
|
||||
}, [getValues, setValue, refresh]);
|
||||
|
||||
// 提交保存模型修改
|
||||
const saveSubmitSuccess = useCallback(
|
||||
async (data: AppSchema) => {
|
||||
setBtnLoading(true);
|
||||
try {
|
||||
await putAppById(data._id, {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
intro: data.intro,
|
||||
chat: data.chat,
|
||||
share: data.share
|
||||
});
|
||||
|
||||
refreshModel.updateModelDetail(data);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setBtnLoading(false);
|
||||
},
|
||||
[refreshModel, toast]
|
||||
);
|
||||
// 提交保存表单失败
|
||||
const saveSubmitError = useCallback(() => {
|
||||
// deep search message
|
||||
const deepSearch = (obj: any): string => {
|
||||
if (!obj) return '提交表单错误';
|
||||
if (!!obj.message) {
|
||||
return obj.message;
|
||||
}
|
||||
return deepSearch(Object.values(obj)[0]);
|
||||
};
|
||||
toast({
|
||||
title: deepSearch(errors),
|
||||
status: 'error',
|
||||
duration: 4000,
|
||||
isClosable: true
|
||||
});
|
||||
}, [errors, toast]);
|
||||
|
||||
const saveUpdateModel = useCallback(
|
||||
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
|
||||
[handleSubmit, saveSubmitError, saveSubmitSuccess]
|
||||
);
|
||||
|
||||
/* 点击删除 */
|
||||
const handleDelModel = useCallback(async () => {
|
||||
if (!appDetail) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delModelById(appDetail._id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success'
|
||||
});
|
||||
refreshModel.removeModelDetail(appDetail._id);
|
||||
router.replace(`/model?modelId=${myApps[1]?._id}`);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '删除失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [appDetail, setIsLoading, toast, refreshModel, router, myApps]);
|
||||
|
||||
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, '头像选择异常'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[setValue, toast]
|
||||
);
|
||||
|
||||
// load model data
|
||||
const { isLoading } = useQuery([modelId], () => loadAppDetail(modelId, true), {
|
||||
onSuccess(res) {
|
||||
res && reset(res);
|
||||
modelId && setLastModelId(modelId);
|
||||
setRefresh(!refresh);
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
title: err?.message || '获取应用异常',
|
||||
status: 'error'
|
||||
});
|
||||
setLastModelId('');
|
||||
refreshModel.freshMyModels();
|
||||
router.replace('/model');
|
||||
}
|
||||
});
|
||||
|
||||
const { data: chatModelList = [] } = useQuery(['initChatModelList'], getChatModelList);
|
||||
|
||||
return (
|
||||
<Box
|
||||
pb={3}
|
||||
px={[5, '25px', '50px']}
|
||||
fontSize={['sm', 'lg']}
|
||||
maxW={['auto', '800px']}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
头像
|
||||
</Box>
|
||||
<Avatar
|
||||
src={getValues('avatar')}
|
||||
w={['32px', '40px']}
|
||||
h={['32px', '40px']}
|
||||
cursor={isOwner ? 'pointer' : 'default'}
|
||||
title={'点击切换头像'}
|
||||
onClick={() => isOwner && onOpenSelectFile()}
|
||||
/>
|
||||
</Flex>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
名称
|
||||
</Box>
|
||||
<Input
|
||||
isDisabled={!isOwner}
|
||||
{...register('name', {
|
||||
required: '展示名称不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<Flex mt={5} alignItems={'flex-start'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
介绍
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
placeholder={'给你的 AI 应用一个介绍'}
|
||||
{...register('intro')}
|
||||
></Textarea>
|
||||
</Flex>
|
||||
|
||||
<Divider mt={5} />
|
||||
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
对话模型
|
||||
</Box>
|
||||
<MySelect
|
||||
width={['90%', '280px']}
|
||||
value={getValues('chat.chatModel')}
|
||||
list={chatModelList.map((item) => ({
|
||||
value: item.chatModel,
|
||||
label: `${item.name} (${formatPrice(
|
||||
ChatModelMap[item.chatModel]?.price,
|
||||
1000
|
||||
)} 元/1k tokens)`
|
||||
}))}
|
||||
onchange={(val: any) => {
|
||||
setValue('chat.chatModel', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} my={10}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
温度
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '严谨', value: 0 },
|
||||
{ label: '发散', value: 10 }
|
||||
]}
|
||||
width={['90%', '260px']}
|
||||
min={0}
|
||||
max={10}
|
||||
value={getValues('chat.temperature')}
|
||||
onChange={(val) => {
|
||||
setValue('chat.temperature', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={12} mb={10}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
回复上限
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${tokenLimit}`, value: tokenLimit }
|
||||
]}
|
||||
width={['90%', '260px']}
|
||||
min={100}
|
||||
max={tokenLimit}
|
||||
step={50}
|
||||
value={getValues('chat.maxToken')}
|
||||
onChange={(val) => {
|
||||
setValue('chat.maxToken', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={10} alignItems={'flex-start'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
提示词
|
||||
<Tooltip label={systemPromptTip}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={8}
|
||||
placeholder={systemPromptTip}
|
||||
{...register('chat.systemPrompt')}
|
||||
></Textarea>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'flex-start'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
限定词
|
||||
<Tooltip label={limitPromptTip}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder={limitPromptTip}
|
||||
{...register('chat.limitPrompt')}
|
||||
></Textarea>
|
||||
</Flex>
|
||||
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
|
||||
<Button
|
||||
mr={3}
|
||||
w={'120px'}
|
||||
size={['sm', 'md']}
|
||||
isLoading={btnLoading}
|
||||
isDisabled={!isOwner}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await saveUpdateModel();
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
error;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isOwner ? '保存' : '仅读,无法修改'}
|
||||
</Button>
|
||||
<Button
|
||||
mr={3}
|
||||
w={'100px'}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
color={'myBlue.600'}
|
||||
borderColor={'myBlue.600'}
|
||||
isLoading={btnLoading}
|
||||
onClick={async () => {
|
||||
try {
|
||||
router.prefetch('/chat');
|
||||
await saveUpdateModel();
|
||||
} catch (error) {}
|
||||
router.push(`/chat?modelId=${modelId}`);
|
||||
}}
|
||||
>
|
||||
对话
|
||||
</Button>
|
||||
{isOwner && (
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
variant={'base'}
|
||||
size={['sm', 'md']}
|
||||
isLoading={btnLoading}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={openConfirm(handleDelModel)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<File onSelect={onSelectFile} />
|
||||
<ConfirmChild />
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
281
client/src/pages/app/detail/components/Share.tsx
Normal file
281
client/src/pages/app/detail/components/Share.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Tooltip,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
FormControl,
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
SliderMark,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getShareChatList, delShareChatById, createShareChat } from '@/api/chat';
|
||||
import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { defaultShareChat } from '@/constants/model';
|
||||
import type { ShareChatEditType } from '@/types/model';
|
||||
|
||||
const Share = ({ modelId }: { modelId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { copyData } = useCopyData();
|
||||
const {
|
||||
isOpen: isOpenCreateShareChat,
|
||||
onOpen: onOpenCreateShareChat,
|
||||
onClose: onCloseCreateShareChat
|
||||
} = useDisclosure();
|
||||
const {
|
||||
register: registerShareChat,
|
||||
getValues: getShareChatValues,
|
||||
setValue: setShareChatValues,
|
||||
handleSubmit: submitShareChat,
|
||||
reset: resetShareChat
|
||||
} = useForm({
|
||||
defaultValues: defaultShareChat
|
||||
});
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
data: shareChatList = [],
|
||||
refetch: refetchShareChatList
|
||||
} = useQuery(['initShareChatList', modelId], () => getShareChatList(modelId));
|
||||
|
||||
const onclickCreateShareChat = useCallback(
|
||||
async (e: ShareChatEditType) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const id = await createShareChat({
|
||||
...e,
|
||||
modelId
|
||||
});
|
||||
onCloseCreateShareChat();
|
||||
refetchShareChatList();
|
||||
|
||||
const url = `对话地址为:${location.origin}/chat/share?shareId=${id}
|
||||
${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
copyData(url, '已复制分享地址');
|
||||
|
||||
resetShareChat(defaultShareChat);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: getErrText(err, '创建分享链接异常'),
|
||||
status: 'warning'
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[
|
||||
copyData,
|
||||
modelId,
|
||||
onCloseCreateShareChat,
|
||||
refetchShareChatList,
|
||||
resetShareChat,
|
||||
setIsLoading,
|
||||
toast
|
||||
]
|
||||
);
|
||||
|
||||
// format share used token
|
||||
const formatTokens = (tokens: number) => {
|
||||
if (tokens < 10000) return tokens;
|
||||
return `${(tokens / 10000).toFixed(2)}万`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
免登录聊天窗口
|
||||
<Tooltip label="可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的tokens。请保管好链接和密码。">
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant={'base'}
|
||||
colorScheme={'myBlue'}
|
||||
size={['sm', 'md']}
|
||||
{...(shareChatList.length >= 10
|
||||
? {
|
||||
isDisabled: true,
|
||||
title: '最多创建10组'
|
||||
}
|
||||
: {})}
|
||||
onClick={onOpenCreateShareChat}
|
||||
>
|
||||
创建新窗口
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer mt={3}>
|
||||
<Table variant={'simple'} w={'100%'} overflowX={'auto'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>名称</Th>
|
||||
<Th>密码</Th>
|
||||
<Th>最大上下文</Th>
|
||||
<Th>tokens消耗</Th>
|
||||
<Th>最后使用时间</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{shareChatList.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.name}</Td>
|
||||
<Td>{item.password === '1' ? '已开启' : '未使用'}</Td>
|
||||
<Td>{item.maxContext}</Td>
|
||||
<Td>{formatTokens(item.tokens)}</Td>
|
||||
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
||||
<Td>
|
||||
<Flex>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name="copy"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item._id}`;
|
||||
copyData(url, '已复制分享地址');
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name="delete"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red' }}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{shareChatList.length === 0 && !isFetching && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
没有创建分享链接
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{/* create shareChat modal */}
|
||||
<Modal isOpen={isOpenCreateShareChat} onClose={onCloseCreateShareChat}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>创建免登录窗口</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<FormControl>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'} w={0}>
|
||||
名称:
|
||||
</Box>
|
||||
<Input
|
||||
placeholder="记录名字,仅用于展示"
|
||||
maxLength={20}
|
||||
{...registerShareChat('name', {
|
||||
required: '记录名称不能为空'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'} w={0}>
|
||||
密码:
|
||||
</Box>
|
||||
<Input placeholder={'不设置密码,可直接访问'} {...registerShareChat('password')} />
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} ml={'60px'} color={'myGray.600'}>
|
||||
密码不会再次展示,请记住你的密码
|
||||
</Box>
|
||||
</FormControl>
|
||||
<FormControl mt={9}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 120px'} w={0}>
|
||||
最长上下文(组)
|
||||
</Box>
|
||||
<Slider
|
||||
aria-label="slider-ex-1"
|
||||
min={1}
|
||||
max={20}
|
||||
step={1}
|
||||
value={getShareChatValues('maxContext')}
|
||||
onChange={(e) => {
|
||||
setShareChatValues('maxContext', e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
>
|
||||
<SliderMark
|
||||
value={getShareChatValues('maxContext')}
|
||||
textAlign="center"
|
||||
bg="myBlue.600"
|
||||
color="white"
|
||||
w={'18px'}
|
||||
h={'18px'}
|
||||
borderRadius={'100px'}
|
||||
fontSize={'xs'}
|
||||
transform={'translate(-50%, -200%)'}
|
||||
>
|
||||
{getShareChatValues('maxContext')}
|
||||
</SliderMark>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack bg={'myBlue.700'} />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={submitShareChat(onclickCreateShareChat)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Share;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from './modules/Divider';
|
||||
import Container from './modules/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
|
||||
const NodeAnswer = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'400px'}
|
||||
logo={'/icon/logo.png'}
|
||||
name={'SSE 响应'}
|
||||
moduleId={moduleId}
|
||||
{...props}
|
||||
>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeAnswer);
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from './modules/Divider';
|
||||
import Container from './modules/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||
|
||||
const NodeChat = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
const outputsLen = useMemo(
|
||||
() => outputs.filter((item) => item.type !== FlowOutputItemTypeEnum.hidden).length,
|
||||
[outputs]
|
||||
);
|
||||
return (
|
||||
<NodeCard minW={'400px'} logo={'/icon/logo.png'} name={'对话'} moduleId={moduleId} {...props}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||
</Container>
|
||||
{outputsLen > 0 && (
|
||||
<>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput flowOutputList={outputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeChat);
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from './modules/Divider';
|
||||
import Container from './modules/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
|
||||
const NodeHistory = ({
|
||||
data: { inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard minW={'300px'} {...props}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput moduleId={props.moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeHistory);
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from './modules/Divider';
|
||||
import Container from './modules/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import KBSelect from './Plugins/KBSelect';
|
||||
|
||||
const NodeKbSearch = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'400px'}
|
||||
logo={'/icon/logo.png'}
|
||||
name={'知识库搜索'}
|
||||
moduleId={moduleId}
|
||||
{...props}
|
||||
>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
kb_ids: ({ key, value, onChangeNode }) => (
|
||||
<KBSelect
|
||||
relatedKbs={value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key,
|
||||
value: e
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
<Divider text="Output" />
|
||||
<Container>
|
||||
<RenderOutput flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeKbSearch);
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Container from './modules/Container';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
const QuestionInputNode = ({
|
||||
data: { inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard minW={'220px'} {...props}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
|
||||
<Box>用户问题</Box>
|
||||
<Handle
|
||||
style={{
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
transform: 'translate(50%,-5px)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
id={SystemInputEnum.userChatInput}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
/>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(QuestionInputNode);
|
||||
@@ -0,0 +1,78 @@
|
||||
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/flow';
|
||||
import Divider from './modules/Divider';
|
||||
import Container from './modules/Container';
|
||||
import Label from './modules/Label';
|
||||
|
||||
const NodeTFSwitch = ({ data: { inputs, outputs, ...props } }: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard minW={'220px'} logo={'/icon/logo.png'} name={'TF 开关'} {...props}>
|
||||
<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);
|
||||
@@ -0,0 +1,143 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const KBSelect = ({
|
||||
relatedKbs = [],
|
||||
onChange
|
||||
}: {
|
||||
relatedKbs: string[];
|
||||
onChange: (e: string[]) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { myKbList, loadKbList } = useUserStore();
|
||||
const [selectedIdList, setSelectedIdList] = useState<string[]>(relatedKbs);
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const showKbList = useMemo(
|
||||
() => myKbList.filter((item) => relatedKbs.includes(item._id)),
|
||||
[myKbList, relatedKbs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadKbList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid gridTemplateColumns={'1fr 1fr'} gridGap={4}>
|
||||
<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} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
w={'800px'}
|
||||
maxW={'90vw'}
|
||||
h={['90vh', 'auto']}
|
||||
>
|
||||
<ModalHeader>关联的知识库({selectedIdList.length})</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody
|
||||
flex={['1 0 0', '0 0 auto']}
|
||||
maxH={'80vh'}
|
||||
overflowY={'auto'}
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
|
||||
gridGap={3}
|
||||
>
|
||||
{myKbList.map((item) => (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
order={relatedKbs.includes(item._id) ? 0 : 1}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
{...(selectedIdList.includes(item._id)
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
let ids = [...selectedIdList];
|
||||
if (!selectedIdList.includes(item._id)) {
|
||||
ids = ids.concat(item._id);
|
||||
} else {
|
||||
const i = ids.findIndex((id) => id === item._id);
|
||||
ids.splice(i, 1);
|
||||
}
|
||||
|
||||
ids = ids.filter((id) => myKbList.find((item) => item._id === id));
|
||||
setSelectedIdList(ids);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCloseKbSelect();
|
||||
onChange(selectedIdList);
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default KBSelect;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
|
||||
import type { AppModuleTemplateItemType } from '@/types/app';
|
||||
import type { XYPosition } from 'reactflow';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const ModuleStoreList = ({
|
||||
isOpen,
|
||||
onAddNode
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onAddNode: (e: { template: AppModuleTemplateItemType; position: XYPosition }) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
left={0}
|
||||
h={isOpen ? '90%' : '0'}
|
||||
w={isOpen ? '360px' : '0'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'20px'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
px={'15px'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||
添加模块
|
||||
</Box>
|
||||
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||
{ModuleTemplates.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 < 400) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleStoreList;
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from 'reactflow';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
export default function 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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 Container;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
|
||||
const Divider = ({ text }: { text: 'Body' | 'Input' | 'Output' | string }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
textAlign={'center'}
|
||||
bg={'#f8f8f8'}
|
||||
py={2}
|
||||
borderTop={theme.borders.base}
|
||||
borderBottom={theme.borders.base}
|
||||
fontSize={'lg'}
|
||||
>
|
||||
{text}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Divider;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Box, Tooltip } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
|
||||
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 && (
|
||||
<Tooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default Label;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import type { FlowModuleItemType } from '@/types/flow';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode | React.ReactNode[] | string;
|
||||
logo?: string;
|
||||
name?: string;
|
||||
intro?: string;
|
||||
minW?: string | number;
|
||||
moduleId: string;
|
||||
onDelNode: FlowModuleItemType['onDelNode'];
|
||||
};
|
||||
|
||||
const NodeCard = ({
|
||||
children,
|
||||
logo = '/icon/logo.png',
|
||||
name = '未知模块',
|
||||
minW = '300px',
|
||||
onDelNode,
|
||||
moduleId
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box minW={minW} 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'} w={'30px'} h={'30px'} />
|
||||
<Box ml={3} flex={1} fontSize={'lg'} color={'myGray.600'}>
|
||||
{name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
className={'nodrag'}
|
||||
name="delete"
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
w={'16px'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => onDelNode(moduleId)}
|
||||
/>
|
||||
</Flex>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeCard);
|
||||
@@ -0,0 +1,182 @@
|
||||
import React from 'react';
|
||||
import type { FlowInputItemType, FlowModuleItemType } from '@/types/flow';
|
||||
import {
|
||||
Box,
|
||||
Textarea,
|
||||
Input,
|
||||
Tooltip,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper
|
||||
} from '@chakra-ui/react';
|
||||
import { FlowInputItemTypeEnum } from '@/constants/flow';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
|
||||
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 && (
|
||||
<Tooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const RenderBody = ({
|
||||
flowInputList,
|
||||
moduleId,
|
||||
CustomComponent = {},
|
||||
onChangeNode
|
||||
}: {
|
||||
flowInputList: FlowInputItemType[];
|
||||
moduleId: string;
|
||||
CustomComponent?: Record<
|
||||
string,
|
||||
(e: {
|
||||
key: string;
|
||||
value: any;
|
||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||
}) => React.ReactNode
|
||||
>;
|
||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{flowInputList.map(
|
||||
(item) =>
|
||||
item.type !== FlowInputItemTypeEnum.hidden && (
|
||||
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||
<Label required={item.required} description={item.description}>
|
||||
{item.label}
|
||||
</Label>
|
||||
<Box mt={2} className={'nodrag'}>
|
||||
{item.type === FlowInputItemTypeEnum.numberInput && (
|
||||
<NumberInput
|
||||
defaultValue={item.value}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: item.key,
|
||||
value: Number(e)
|
||||
});
|
||||
}}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.input && (
|
||||
<Input
|
||||
placeholder={item.placeholder}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: item.key,
|
||||
value: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.textarea && (
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder={item.placeholder}
|
||||
resize={'both'}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: item.key,
|
||||
value: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={item.list || []}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: item.key,
|
||||
value: e
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.slider && (
|
||||
<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,
|
||||
key: item.key,
|
||||
value: e
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
|
||||
<>
|
||||
{CustomComponent[item.key]({ key: item.key, value: item.value, onChangeNode })}
|
||||
</>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.target && (
|
||||
<Handle
|
||||
style={{
|
||||
top: '50%',
|
||||
left: '-14px',
|
||||
transform: 'translate(-50%,-50%)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
id={item.key}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
onConnect={(params) => console.log('handle onConnect', params)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderBody);
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import type { FlowOutputItemType } from '@/types/flow';
|
||||
import { Box, Tooltip, Flex } from '@chakra-ui/react';
|
||||
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
const Label = ({
|
||||
children,
|
||||
description
|
||||
}: {
|
||||
children: React.ReactNode | string;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Flex as={'label'} justifyContent={'right'} alignItems={'center'} position={'relative'}>
|
||||
{description && (
|
||||
<Tooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
const RenderBody = ({ flowOutputList }: { flowOutputList: FlowOutputItemType[] }) => {
|
||||
return (
|
||||
<>
|
||||
{flowOutputList.map(
|
||||
(item) =>
|
||||
item.type !== FlowOutputItemTypeEnum.hidden && (
|
||||
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||
<Label description={item.description}>{item.label}</Label>
|
||||
<Box mt={FlowOutputItemTypeEnum.answer ? 0 : 2} className={'nodrag'}>
|
||||
{item.type === FlowOutputItemTypeEnum.source && (
|
||||
<Handle
|
||||
style={{
|
||||
top: '50%',
|
||||
right: '-14px',
|
||||
transform: 'translate(50%,-50%)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
type="source"
|
||||
id={item.key}
|
||||
position={Position.Right}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderBody);
|
||||
@@ -0,0 +1,5 @@
|
||||
.panel {
|
||||
.react-flow__panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
312
client/src/pages/app/detail/components/edit/index.tsx
Normal file
312
client/src/pages/app/detail/components/edit/index.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
XYPosition,
|
||||
Connection
|
||||
} from 'reactflow';
|
||||
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { edgeOptions, connectionLineStyle, FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { appModule2FlowNode, appModule2FlowEdge } from '@/utils/adapt';
|
||||
import {
|
||||
FlowModuleItemType,
|
||||
FlowOutputTargetItemType,
|
||||
type FlowModuleItemChangeProps
|
||||
} from '@/types/flow';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { putAppById } from '@/api/app';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeKbSearch = dynamic(() => import('./components/NodeKbSearch'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeHistory = dynamic(() => import('./components/NodeHistory'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeTFSwitch = dynamic(() => import('./components/NodeTFSwitch'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeAnswer = dynamic(() => import('./components/NodeAnswer'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput'), {
|
||||
ssr: false
|
||||
});
|
||||
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
import { AppModuleTemplateItemType } from '@/types/app';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const nodeTypes = {
|
||||
[FlowModuleTypeEnum.questionInputNode]: NodeQuestionInput,
|
||||
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
||||
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
|
||||
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
|
||||
[FlowModuleTypeEnum.answerNode]: NodeAnswer
|
||||
};
|
||||
const edgeTypes = {
|
||||
buttonedge: ButtonEdge
|
||||
};
|
||||
|
||||
const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
const theme = useTheme();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== moduleId) return node;
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.map((item) => {
|
||||
if (item.key === key) {
|
||||
return {
|
||||
...item,
|
||||
[valueKey]: value
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
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 onAddNode = useCallback(
|
||||
({ template, position }: { template: AppModuleTemplateItemType; position: XYPosition }) => {
|
||||
setNodes((state) =>
|
||||
state.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
position,
|
||||
moduleId: nanoid()
|
||||
},
|
||||
onChangeNode,
|
||||
onDelNode
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[onChangeNode, onDelNode, setNodes]
|
||||
);
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
(id: string) => {
|
||||
setEdges((state) => state.filter((item) => item.id !== id));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: 'buttonedge',
|
||||
animated: true,
|
||||
data: {
|
||||
onDelete: onDelConnect
|
||||
}
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[onDelConnect, setEdges]
|
||||
);
|
||||
|
||||
const { mutate: onclickSave, isLoading } = useRequest({
|
||||
mutationFn: () => {
|
||||
const modules = nodes.map((item) => ({
|
||||
...item.data,
|
||||
position: item.position,
|
||||
onChangeNode: undefined,
|
||||
onDelNode: undefined,
|
||||
outputs: item.data.outputs.map((output) => ({
|
||||
...output,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
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 putAppById(app._id, {
|
||||
modules
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
errorToast: '保存配置异常'
|
||||
});
|
||||
|
||||
const initData = useCallback(
|
||||
(app: AppSchema) => {
|
||||
console.log('init');
|
||||
|
||||
const edges = appModule2FlowEdge({
|
||||
modules: app.modules,
|
||||
onDelete: onDelConnect
|
||||
});
|
||||
setEdges(edges);
|
||||
|
||||
setNodes(
|
||||
app.modules.map((item) =>
|
||||
appModule2FlowNode({
|
||||
item,
|
||||
onChangeNode,
|
||||
onDelNode
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[onDelConnect, setEdges, setNodes, onChangeNode, onDelNode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initData(JSON.parse(JSON.stringify(app)));
|
||||
}, [app]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
<Flex py={3} px={5} borderBottom={theme.borders.base} alignItems={'center'}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'back'} w={'14px'} />}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'base'}
|
||||
aria-label={''}
|
||||
onClick={onBack}
|
||||
/>
|
||||
<Box ml={5} fontSize={'xl'} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</Flex>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
w={'100%'}
|
||||
h={0}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<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={'1px 1px 6px #4e83fd'}
|
||||
onClick={() => (isOpenTemplate ? onCloseTemplate() : onOpenTemplate())}
|
||||
/>
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
className={styles.panel}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.4}
|
||||
maxZoom={1.5}
|
||||
fitView
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={(connect) => {
|
||||
connect.sourceHandle &&
|
||||
connect.targetHandle &&
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls
|
||||
position={'bottom-center'}
|
||||
style={{ display: 'flex' }}
|
||||
showInteractive={false}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppEdit;
|
||||
127
client/src/pages/app/detail/index.tsx
Normal file
127
client/src/pages/app/detail/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Tabs from '@/components/Tabs';
|
||||
|
||||
import Settings from './components/Settings';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
|
||||
const EditApp = dynamic(() => import('./components/edit'), {
|
||||
ssr: false
|
||||
});
|
||||
const Share = dynamic(() => import('./components/Share'), {
|
||||
ssr: false
|
||||
});
|
||||
const API = dynamic(() => import('./components/API'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
enum TabEnum {
|
||||
'settings' = 'settings',
|
||||
'edit' = 'edit',
|
||||
'share' = 'share',
|
||||
'API' = 'API'
|
||||
}
|
||||
|
||||
const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
const router = useRouter();
|
||||
const { appId } = router.query as { appId: string };
|
||||
const { isPc } = useGlobalStore();
|
||||
const { appDetail = defaultApp, loadAppDetail, userInfo } = useUserStore();
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => appDetail.userId === userInfo?._id,
|
||||
[appDetail.userId, userInfo?._id]
|
||||
);
|
||||
|
||||
const setCurrentTab = useCallback(
|
||||
(tab: `${TabEnum}`) => {
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
appId,
|
||||
currentTab: tab
|
||||
}
|
||||
});
|
||||
},
|
||||
[appId, router]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.onbeforeunload = (e) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = '内容已修改,确认离开页面吗?';
|
||||
};
|
||||
|
||||
return () => {
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAppDetail(appId);
|
||||
}, [appId, loadAppDetail]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
maxW={'100vw'}
|
||||
pt={4}
|
||||
overflow={'overlay'}
|
||||
position={'relative'}
|
||||
bg={'white'}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<Box textAlign={['center', 'left']} px={5} mb={4}>
|
||||
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
<Tabs
|
||||
mx={['auto', '0']}
|
||||
mt={2}
|
||||
w={['300px', '360px']}
|
||||
list={[
|
||||
{ label: '配置', id: TabEnum.settings },
|
||||
...(isOwner ? [{ label: '编排', id: TabEnum.edit }] : []),
|
||||
{ label: '分享', id: TabEnum.share },
|
||||
{ label: 'API', id: TabEnum.API },
|
||||
{ label: '立即对话', id: 'startChat' }
|
||||
]}
|
||||
size={isPc ? 'md' : 'sm'}
|
||||
activeId={currentTab}
|
||||
onChange={(e: any) => {
|
||||
if (e === 'startChat') {
|
||||
router.push(`/chat?modelId=${appId}`);
|
||||
} else {
|
||||
setCurrentTab(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
{currentTab === TabEnum.settings && <Settings modelId={appId} />}
|
||||
{currentTab === TabEnum.edit && (
|
||||
<Box position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<EditApp app={appDetail} onBack={() => setCurrentTab(TabEnum.settings)} />
|
||||
</Box>
|
||||
)}
|
||||
{currentTab === TabEnum.API && <API modelId={appId} />}
|
||||
{currentTab === TabEnum.share && <Share modelId={appId} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const currentTab = context?.query?.currentTab || TabEnum.settings;
|
||||
|
||||
return {
|
||||
props: { currentTab }
|
||||
};
|
||||
}
|
||||
|
||||
export default AppDetail;
|
||||
26
client/src/pages/app/list/index.tsx
Normal file
26
client/src/pages/app/list/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const MyApps = () => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
className="textlg"
|
||||
borderBottom={theme.borders.base}
|
||||
letterSpacing={1}
|
||||
py={3}
|
||||
px={5}
|
||||
fontSize={'24px'}
|
||||
fontWeight={'bold'}
|
||||
onClick={() => router.push(`/app/detail?appId=642adec15f01d67d4613efdb`)}
|
||||
>
|
||||
我的应用
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyApps;
|
||||
@@ -54,11 +54,8 @@ const PcSliderBar = ({
|
||||
}>();
|
||||
|
||||
const { history, loadHistory } = useChatStore();
|
||||
const { myModels, myCollectionModels, loadMyModels } = useUserStore();
|
||||
const models = useMemo(
|
||||
() => [...myModels, ...myCollectionModels],
|
||||
[myCollectionModels, myModels]
|
||||
);
|
||||
const { myApps, myCollectionApps, loadMyModels } = useUserStore();
|
||||
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
|
||||
|
||||
// custom title edit
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
|
||||
|
||||
@@ -35,13 +35,10 @@ const PhoneSliderBar = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [currentTab, setCurrentTab] = useState(TabEnum.app);
|
||||
const { myModels, myCollectionModels, loadMyModels } = useUserStore();
|
||||
const { myApps, myCollectionApps, loadMyModels } = useUserStore();
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
|
||||
const models = useMemo(
|
||||
() => [...myModels, ...myCollectionModels],
|
||||
[myCollectionModels, myModels]
|
||||
);
|
||||
const models = useMemo(() => [...myApps, ...myCollectionApps], [myCollectionApps, myApps]);
|
||||
useQuery(['loadModels'], () => loadMyModels(false));
|
||||
|
||||
const { history, loadHistory } = useChatStore();
|
||||
|
||||
@@ -175,7 +175,7 @@ const Chat = () => {
|
||||
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
|
||||
|
||||
// 流请求,获取数据
|
||||
const { newChatId, quoteLen, errMsg } = await streamFetch({
|
||||
const { newChatId, errMsg } = await streamFetch({
|
||||
data: {
|
||||
messages,
|
||||
chatId,
|
||||
@@ -203,6 +203,7 @@ const Chat = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// save chat
|
||||
if (newChatId) {
|
||||
setForbidLoadChatData(true);
|
||||
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
|
||||
@@ -219,7 +220,7 @@ const Chat = () => {
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
quoteLen,
|
||||
quoteLen: 0,
|
||||
systemPrompt: `${chatData.systemPrompt}${`${
|
||||
chatData.limitPrompt ? `\n\n${chatData.limitPrompt}` : ''
|
||||
}`}`
|
||||
|
||||
@@ -270,8 +270,8 @@ const SelectFileModal = ({
|
||||
min={300}
|
||||
max={1000}
|
||||
step={50}
|
||||
activeVal={modeMap[TrainingModeEnum.index].maxLen}
|
||||
setVal={(val) => {
|
||||
value={modeMap[TrainingModeEnum.index].maxLen}
|
||||
onChange={(val) => {
|
||||
setModeMap((state) => ({
|
||||
...state,
|
||||
[TrainingModeEnum.index]: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSendCode } from '@/hooks/useSendCode';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useRouter } from 'next/router';
|
||||
import { postCreateModel } from '@/api/model';
|
||||
import { postCreateModel } from '@/api/app';
|
||||
|
||||
interface Props {
|
||||
loginSuccess: (e: ResLogin) => void;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box, Flex, Input, IconButton, Tooltip, useTheme } from '@chakra-ui/reac
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { postCreateModel } from '@/api/model';
|
||||
import { postCreateModel } from '@/api/app';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -25,7 +25,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { myModels, myCollectionModels, loadMyModels, refreshModel } = useUserStore();
|
||||
const { myApps, myCollectionApps, loadMyModels, refreshModel } = useUserStore();
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
/* 加载模型 */
|
||||
@@ -35,7 +35,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const id = await postCreateModel({
|
||||
name: `AI应用${myModels.length + 1}`
|
||||
name: `AI应用${myApps.length + 1}`
|
||||
});
|
||||
toast({
|
||||
title: '创建成功',
|
||||
@@ -50,23 +50,23 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [myModels.length, refreshModel, router, setIsLoading, toast]);
|
||||
}, [myApps.length, refreshModel, router, setIsLoading, toast]);
|
||||
|
||||
const currentModels = useMemo(() => {
|
||||
const map = {
|
||||
[MyModelsTypeEnum.my]: {
|
||||
list: myModels.filter((item) => new RegExp(searchText, 'ig').test(item.name + item.intro)),
|
||||
list: myApps.filter((item) => new RegExp(searchText, 'ig').test(item.name + item.intro)),
|
||||
emptyText: '还没有 AI 应用~\n快来创建一个吧'
|
||||
},
|
||||
[MyModelsTypeEnum.collection]: {
|
||||
list: myCollectionModels.filter((item) =>
|
||||
list: myCollectionApps.filter((item) =>
|
||||
new RegExp(searchText, 'ig').test(item.name + item.intro)
|
||||
),
|
||||
emptyText: '收藏的 AI 应用为空~\n快去市场找一个吧'
|
||||
}
|
||||
};
|
||||
return map[currentTab];
|
||||
}, [currentTab, myCollectionModels, myModels, searchText]);
|
||||
}, [currentTab, myCollectionApps, myApps, searchText]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useUserStore } from '@/store/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { putModelById } from '@/api/model';
|
||||
import { putAppById } from '@/api/app';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -34,15 +34,15 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { modelDetail, loadKbList, loadModelDetail } = useUserStore();
|
||||
const { appDetail, loadKbList, loadAppDetail } = useUserStore();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, reset, getValues, setValue } = useForm({
|
||||
defaultValues: {
|
||||
searchSimilarity: modelDetail.chat.searchSimilarity,
|
||||
searchLimit: modelDetail.chat.searchLimit,
|
||||
searchEmptyText: modelDetail.chat.searchEmptyText
|
||||
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||
searchLimit: appDetail.chat.searchLimit,
|
||||
searchEmptyText: appDetail.chat.searchEmptyText
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,13 +68,13 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await putModelById(modelId, {
|
||||
await putAppById(modelId, {
|
||||
chat: {
|
||||
...modelDetail.chat,
|
||||
...appDetail.chat,
|
||||
...data
|
||||
}
|
||||
});
|
||||
loadModelDetail(modelId, true);
|
||||
loadAppDetail(modelId, true);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
@@ -83,7 +83,7 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setIsLoading, modelId, modelDetail.chat, loadModelDetail, toast]
|
||||
[setIsLoading, modelId, appDetail.chat, loadAppDetail, toast]
|
||||
);
|
||||
|
||||
// init kb select list
|
||||
@@ -91,10 +91,10 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
|
||||
return (
|
||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||
<Box fontWeight={'bold'}>关联的知识库({modelDetail.chat?.relatedKbs.length})</Box>
|
||||
<Box fontWeight={'bold'}>关联的知识库({appDetail.chat?.relatedKbs.length})</Box>
|
||||
{(() => {
|
||||
const kbs =
|
||||
modelDetail.chat?.relatedKbs
|
||||
appDetail.chat?.relatedKbs
|
||||
?.map((id) => kbList.find((kb) => kb._id === id))
|
||||
.filter((item) => item) || [];
|
||||
return (
|
||||
@@ -120,9 +120,9 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
}}
|
||||
onClick={() => {
|
||||
reset({
|
||||
searchSimilarity: modelDetail.chat.searchSimilarity,
|
||||
searchLimit: modelDetail.chat.searchLimit,
|
||||
searchEmptyText: modelDetail.chat.searchEmptyText
|
||||
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||
searchLimit: appDetail.chat.searchLimit,
|
||||
searchEmptyText: appDetail.chat.searchEmptyText
|
||||
});
|
||||
onOpenEditParams();
|
||||
}}
|
||||
@@ -139,9 +139,9 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
调整搜索参数
|
||||
</Flex>
|
||||
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
||||
相似度: {modelDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
||||
{modelDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
||||
{modelDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
|
||||
相似度: {appDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
||||
{appDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
||||
{appDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card
|
||||
@@ -156,7 +156,7 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedIdList(
|
||||
modelDetail.chat?.relatedKbs ? [...modelDetail.chat?.relatedKbs] : []
|
||||
appDetail.chat?.relatedKbs ? [...appDetail.chat?.relatedKbs] : []
|
||||
);
|
||||
onOpenKbSelect();
|
||||
}}
|
||||
@@ -219,8 +219,8 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
size={'sm'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
const ids = modelDetail.chat?.relatedKbs
|
||||
? [...modelDetail.chat.relatedKbs]
|
||||
const ids = appDetail.chat?.relatedKbs
|
||||
? [...appDetail.chat.relatedKbs]
|
||||
: [];
|
||||
const i = ids.findIndex((id) => id === item._id);
|
||||
ids.splice(i, 1);
|
||||
@@ -262,7 +262,7 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
order={modelDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
|
||||
order={appDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
@@ -328,8 +328,8 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
activeVal={getValues('searchSimilarity')}
|
||||
setVal={(val) => {
|
||||
value={getValues('searchSimilarity')}
|
||||
onChange={(val) => {
|
||||
setValue('searchSimilarity', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
@@ -345,8 +345,8 @@ const Kb = ({ modelId }: { modelId: string }) => {
|
||||
]}
|
||||
min={1}
|
||||
max={20}
|
||||
activeVal={getValues('searchLimit')}
|
||||
setVal={(val) => {
|
||||
value={getValues('searchLimit')}
|
||||
onChange={(val) => {
|
||||
setValue('searchLimit', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { delModelById, putModelById } from '@/api/model';
|
||||
import { delModelById, putAppById } from '@/api/app';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
@@ -24,7 +24,7 @@ import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { ChatModelMap, chatModelList } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MySelect from '@/components/Select';
|
||||
@@ -39,7 +39,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { userInfo, modelDetail, myModels, loadModelDetail, refreshModel, setLastModelId } =
|
||||
const { userInfo, appDetail, myApps, loadAppDetail, refreshModel, setLastModelId } =
|
||||
useUserStore();
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
@@ -60,12 +60,12 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
defaultValues: modelDetail
|
||||
defaultValues: appDetail
|
||||
});
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => modelDetail.userId === userInfo?._id,
|
||||
[modelDetail.userId, userInfo?._id]
|
||||
() => appDetail.userId === userInfo?._id,
|
||||
[appDetail.userId, userInfo?._id]
|
||||
);
|
||||
const tokenLimit = useMemo(() => {
|
||||
const max = ChatModelMap[getValues('chat.chatModel')]?.contextMaxToken || 4000;
|
||||
@@ -79,10 +79,10 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
|
||||
// 提交保存模型修改
|
||||
const saveSubmitSuccess = useCallback(
|
||||
async (data: ModelSchema) => {
|
||||
async (data: AppSchema) => {
|
||||
setBtnLoading(true);
|
||||
try {
|
||||
await putModelById(data._id, {
|
||||
await putAppById(data._id, {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
intro: data.intro,
|
||||
@@ -126,16 +126,16 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
|
||||
/* 点击删除 */
|
||||
const handleDelModel = useCallback(async () => {
|
||||
if (!modelDetail) return;
|
||||
if (!appDetail) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delModelById(modelDetail._id);
|
||||
await delModelById(appDetail._id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success'
|
||||
});
|
||||
refreshModel.removeModelDetail(modelDetail._id);
|
||||
router.replace(`/model?modelId=${myModels[1]?._id}`);
|
||||
refreshModel.removeModelDetail(appDetail._id);
|
||||
router.replace(`/model?modelId=${myApps[1]?._id}`);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '删除失败',
|
||||
@@ -143,7 +143,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [modelDetail, setIsLoading, toast, refreshModel, router, myModels]);
|
||||
}, [appDetail, setIsLoading, toast, refreshModel, router, myApps]);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
@@ -168,7 +168,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
);
|
||||
|
||||
// load model data
|
||||
const { isLoading } = useQuery([modelId], () => loadModelDetail(modelId, true), {
|
||||
const { isLoading } = useQuery([modelId], () => loadAppDetail(modelId, true), {
|
||||
onSuccess(res) {
|
||||
res && reset(res);
|
||||
modelId && setLastModelId(modelId);
|
||||
@@ -241,7 +241,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
width={['100%', '300px']}
|
||||
value={getValues('chat.chatModel')}
|
||||
list={chatModelList.map((item) => ({
|
||||
id: item.chatModel,
|
||||
value: item.chatModel,
|
||||
label: `${item.name} (${formatPrice(
|
||||
ChatModelMap[item.chatModel]?.price,
|
||||
1000
|
||||
@@ -266,8 +266,8 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
width={['95%', '280px']}
|
||||
min={0}
|
||||
max={10}
|
||||
activeVal={getValues('chat.temperature')}
|
||||
setVal={(val) => {
|
||||
value={getValues('chat.temperature')}
|
||||
onChange={(val) => {
|
||||
setValue('chat.temperature', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
@@ -288,8 +288,8 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
min={100}
|
||||
max={tokenLimit}
|
||||
step={50}
|
||||
activeVal={getValues('chat.maxToken')}
|
||||
setVal={(val) => {
|
||||
value={getValues('chat.maxToken')}
|
||||
onChange={(val) => {
|
||||
setValue('chat.maxToken', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
|
||||
@@ -7,7 +7,7 @@ import dynamic from 'next/dynamic';
|
||||
import Tabs from '@/components/Tabs';
|
||||
|
||||
import Settings from './components/Settings';
|
||||
import { defaultModel } from '@/constants/model';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
|
||||
const Kb = dynamic(() => import('./components/Kb'), {
|
||||
ssr: false
|
||||
@@ -29,12 +29,12 @@ enum TabEnum {
|
||||
const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
const router = useRouter();
|
||||
const { isPc } = useGlobalStore();
|
||||
const { modelDetail = defaultModel, userInfo } = useUserStore();
|
||||
const { appDetail = defaultApp, userInfo } = useUserStore();
|
||||
const [currentTab, setCurrentTab] = useState<`${TabEnum}`>(TabEnum.settings);
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => modelDetail.userId === userInfo?._id,
|
||||
[modelDetail.userId, userInfo?._id]
|
||||
() => appDetail.userId === userInfo?._id,
|
||||
[appDetail.userId, userInfo?._id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,7 +65,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
{/* 头部 */}
|
||||
<Box textAlign={['center', 'left']} px={5} mb={4}>
|
||||
<Box className="textlg" display={['block', 'none']} fontSize={'3xl'} fontWeight={'bold'}>
|
||||
{modelDetail.name}
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
<Tabs
|
||||
mx={['auto', '0']}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { Box, Flex, Card, Grid, Input } from '@chakra-ui/react';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { getShareModelList, triggerModelCollection } from '@/api/model';
|
||||
import { getShareModelList, triggerModelCollection } from '@/api/app';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import type { ShareModelItem } from '@/types/model';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
Reference in New Issue
Block a user