mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
feat: overview setting
This commit is contained in:
@@ -650,9 +650,9 @@ const ChatBox = (
|
||||
<Box
|
||||
py={'18px'}
|
||||
position={'relative'}
|
||||
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
|
||||
boxShadow={`0 0 10px rgba(0,0,0,0.2)`}
|
||||
borderTop={['1px solid', 0]}
|
||||
borderTopColor={'gray.200'}
|
||||
borderTopColor={'myGray.200'}
|
||||
borderRadius={['none', 'md']}
|
||||
backgroundColor={'white'}
|
||||
>
|
||||
|
@@ -9,6 +9,14 @@ import {
|
||||
} from './inputTemplate';
|
||||
import { rawSearchKey } from '../chat';
|
||||
|
||||
export const ChatModelSystemTip =
|
||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可使用变量,例如 {{language}}';
|
||||
export const ChatModelLimitTip =
|
||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。可使用变量,例如 {{language}}。引导例子:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"';
|
||||
export const userGuideTip = '可以添加特殊的对话前后引导模块,更好的让用户进行对话';
|
||||
export const welcomeTextTip =
|
||||
'每次对话开始前,发送一个初始内容。支持标准 Markdown 语法,可使用的额外标记:\n[快捷按键]: 用户点击后可以直接发送该问题';
|
||||
|
||||
export const VariableModule: AppModuleTemplateItemType = {
|
||||
logo: '/imgs/module/variable.png',
|
||||
name: '全局变量',
|
||||
@@ -30,7 +38,7 @@ export const VariableModule: AppModuleTemplateItemType = {
|
||||
export const UserGuideModule: AppModuleTemplateItemType = {
|
||||
logo: '/imgs/module/userGuide.png',
|
||||
name: '用户引导',
|
||||
intro: '可以添加特殊的对话前后引导模块,更好的让用户进行对话',
|
||||
intro: userGuideTip,
|
||||
type: AppModuleItemTypeEnum.userGuide,
|
||||
flowType: FlowModuleTypeEnum.userGuide,
|
||||
inputs: [
|
||||
@@ -77,7 +85,7 @@ export const HistoryModule: AppModuleTemplateItemType = {
|
||||
key: 'maxContext',
|
||||
type: FlowInputItemTypeEnum.numberInput,
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
value: 6,
|
||||
min: 0,
|
||||
max: 50
|
||||
},
|
||||
@@ -146,20 +154,16 @@ export const ChatModule: AppModuleTemplateItemType = {
|
||||
key: 'systemPrompt',
|
||||
type: FlowInputItemTypeEnum.textarea,
|
||||
label: '系统提示词',
|
||||
description:
|
||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。',
|
||||
placeholder:
|
||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。',
|
||||
description: ChatModelSystemTip,
|
||||
placeholder: ChatModelSystemTip,
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
key: 'limitPrompt',
|
||||
type: FlowInputItemTypeEnum.textarea,
|
||||
label: '限定词',
|
||||
description:
|
||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"',
|
||||
placeholder:
|
||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"',
|
||||
description: ChatModelLimitTip,
|
||||
placeholder: ChatModelLimitTip,
|
||||
value: ''
|
||||
},
|
||||
// Input_Template_TFSwitch,
|
||||
@@ -191,7 +195,7 @@ export const KBSearchModule: AppModuleTemplateItemType = {
|
||||
url: '/app/modules/kb/search',
|
||||
inputs: [
|
||||
{
|
||||
key: 'kb_ids',
|
||||
key: 'kbList',
|
||||
type: FlowInputItemTypeEnum.custom,
|
||||
label: '关联的知识库',
|
||||
value: [],
|
||||
@@ -548,7 +552,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
value: 10,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
@@ -627,7 +631,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
value: 10,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
@@ -788,7 +792,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
|
||||
...KBSearchModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'kb_ids',
|
||||
key: 'kbList',
|
||||
type: 'custom',
|
||||
label: '关联的知识库',
|
||||
value: [],
|
||||
@@ -1100,7 +1104,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
value: 10,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
@@ -1239,7 +1243,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
value: 10,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
@@ -1400,7 +1404,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
|
||||
...KBSearchModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'kb_ids',
|
||||
key: 'kbList',
|
||||
type: 'custom',
|
||||
label: '关联的知识库',
|
||||
value: [],
|
||||
|
@@ -163,6 +163,9 @@ const Textarea: ComponentStyleConfig = {
|
||||
border: '1px solid',
|
||||
borderRadius: 'base',
|
||||
borderColor: 'myGray.200',
|
||||
_hover: {
|
||||
borderColor: ''
|
||||
},
|
||||
_focus: {
|
||||
borderColor: 'myBlue.600',
|
||||
boxShadow: '0px 0px 4px #A8DBFF',
|
||||
|
@@ -52,6 +52,49 @@ const chatTemplate = ({
|
||||
},
|
||||
moduleId: '7z5g5h'
|
||||
},
|
||||
{
|
||||
logo: '/imgs/module/history.png',
|
||||
name: '聊天记录',
|
||||
intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。',
|
||||
type: 'initInput',
|
||||
flowType: 'historyNode',
|
||||
url: '/app/modules/init/history',
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'hidden',
|
||||
label: '聊天记录',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
label: '聊天记录',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: '7pacf0',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'xj0c9p'
|
||||
},
|
||||
{
|
||||
logo: '/imgs/module/AI.png',
|
||||
name: 'AI 对话',
|
||||
@@ -176,49 +219,6 @@ const chatTemplate = ({
|
||||
y: 890.014595014464
|
||||
},
|
||||
moduleId: '7pacf0'
|
||||
},
|
||||
{
|
||||
logo: '/imgs/module/history.png',
|
||||
name: '聊天记录',
|
||||
intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。',
|
||||
type: 'initInput',
|
||||
flowType: 'historyNode',
|
||||
url: '/app/modules/init/history',
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 4,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'hidden',
|
||||
label: '聊天记录',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
label: '聊天记录',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: '7pacf0',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'xj0c9p'
|
||||
}
|
||||
];
|
||||
};
|
||||
@@ -228,7 +228,7 @@ const kbTemplate = ({
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbs = [],
|
||||
kbList = [],
|
||||
searchSimilarity,
|
||||
searchLimit,
|
||||
searchEmptyText
|
||||
@@ -238,7 +238,7 @@ const kbTemplate = ({
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
kbs: string[];
|
||||
kbList: { kbId: string }[];
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
@@ -446,10 +446,10 @@ const kbTemplate = ({
|
||||
url: '/app/modules/kb/search',
|
||||
inputs: [
|
||||
{
|
||||
key: 'kb_ids',
|
||||
key: 'kbList',
|
||||
type: 'custom',
|
||||
label: '关联的知识库',
|
||||
value: kbs,
|
||||
value: kbList,
|
||||
list: [],
|
||||
connected: false
|
||||
},
|
||||
@@ -588,54 +588,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 1000 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
const total = await App.countDocuments();
|
||||
let promise = Promise.resolve();
|
||||
console.log(total);
|
||||
|
||||
// 遍历所有的 app
|
||||
const apps = await App.find(
|
||||
{
|
||||
chat: { $ne: null },
|
||||
modules: { $exists: false }
|
||||
// userId: '63f9a14228d2a688d8dc9e1b'
|
||||
},
|
||||
'_id chat'
|
||||
).limit(limit);
|
||||
|
||||
await Promise.all(
|
||||
apps.map(async (app) => {
|
||||
if (!app.chat) return app;
|
||||
const modules = (() => {
|
||||
if (app.chat.relatedKbs.length === 0) {
|
||||
return chatTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt
|
||||
});
|
||||
} else {
|
||||
return kbTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt,
|
||||
kbs: app.chat.relatedKbs,
|
||||
searchEmptyText: app.chat.searchEmptyText,
|
||||
searchLimit: app.chat.searchLimit,
|
||||
searchSimilarity: app.chat.searchSimilarity
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await App.findByIdAndUpdate(app.id, {
|
||||
modules
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit, skipVal))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
return modules;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: apps.length
|
||||
});
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
@@ -643,3 +613,51 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number, skip: number) {
|
||||
// 遍历 app
|
||||
const apps = await App.find(
|
||||
{
|
||||
chat: { $ne: null }
|
||||
// modules: { $exists: false },
|
||||
// userId: '63f9a14228d2a688d8dc9e1b'
|
||||
},
|
||||
'_id chat'
|
||||
)
|
||||
.limit(limit)
|
||||
.skip(skip);
|
||||
|
||||
return Promise.all(
|
||||
apps.map(async (app) => {
|
||||
if (!app.chat) return app;
|
||||
const modules = (() => {
|
||||
if (app.chat.relatedKbs.length === 0) {
|
||||
return chatTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt
|
||||
});
|
||||
} else {
|
||||
return kbTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt,
|
||||
kbList: app.chat.relatedKbs.map((id) => ({ kbId: id })),
|
||||
searchEmptyText: app.chat.searchEmptyText,
|
||||
searchLimit: app.chat.searchLimit,
|
||||
searchSimilarity: app.chat.searchSimilarity
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await App.findByIdAndUpdate(app.id, {
|
||||
modules
|
||||
});
|
||||
return modules;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import { getVector } from '@/pages/api/openapi/plugin/vector';
|
||||
import { countModelPrice, pushTaskBillListItem } from '@/service/events/pushBill';
|
||||
import { getModel } from '@/service/utils/data';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
|
||||
export type QuoteItemType = {
|
||||
kb_id: string;
|
||||
@@ -18,7 +19,7 @@ export type QuoteItemType = {
|
||||
source?: string;
|
||||
};
|
||||
type Props = {
|
||||
kb_ids: string[];
|
||||
kbList: SelectedKbType;
|
||||
history: ChatItemType[];
|
||||
similarity: number;
|
||||
limit: number;
|
||||
@@ -37,19 +38,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
|
||||
const { kb_ids = [], userChatInput } = req.body as Props;
|
||||
const { kbList = [], userChatInput } = req.body as Props;
|
||||
|
||||
if (!userChatInput) {
|
||||
throw new Error('用户输入为空');
|
||||
}
|
||||
|
||||
if (!Array.isArray(kb_ids) || kb_ids.length === 0) {
|
||||
if (!Array.isArray(kbList) || kbList.length === 0) {
|
||||
throw new Error('没有选择知识库');
|
||||
}
|
||||
|
||||
const result = await kbSearch({
|
||||
...req.body,
|
||||
kb_ids,
|
||||
kbList,
|
||||
userChatInput
|
||||
});
|
||||
|
||||
@@ -66,7 +67,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
});
|
||||
|
||||
export async function kbSearch({
|
||||
kb_ids = [],
|
||||
kbList = [],
|
||||
history = [],
|
||||
similarity = 0.8,
|
||||
limit = 5,
|
||||
@@ -74,12 +75,9 @@ export async function kbSearch({
|
||||
userChatInput,
|
||||
billId
|
||||
}: Props): Promise<Response> {
|
||||
if (kb_ids.length === 0)
|
||||
return {
|
||||
isEmpty: true,
|
||||
rawSearch: [],
|
||||
quotePrompt: undefined
|
||||
};
|
||||
if (kbList.length === 0) {
|
||||
return Promise.reject('没有选择知识库');
|
||||
}
|
||||
|
||||
// get vector
|
||||
const vectorModel = global.vectorModels[0].model;
|
||||
@@ -93,8 +91,8 @@ export async function kbSearch({
|
||||
PgClient.query(
|
||||
`BEGIN;
|
||||
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
||||
select kb_id,id,q,a,source from modelData where kb_id IN (${kb_ids
|
||||
.map((item) => `'${item}'`)
|
||||
select kb_id,id,q,a,source from modelData where kb_id IN (${kbList
|
||||
.map((item) => `'${item.kbId}'`)
|
||||
.join(',')}) AND vector <#> '[${vectors[0]}]' < -${similarity} order by vector <#> '[${
|
||||
vectors[0]
|
||||
}]' limit ${limit};
|
||||
|
527
client/src/pages/app/detail/components/BasicEdit/index.tsx
Normal file
527
client/src/pages/app/detail/components/BasicEdit/index.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Grid,
|
||||
BoxProps,
|
||||
Textarea,
|
||||
useTheme,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
useDisclosure,
|
||||
Button,
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import {
|
||||
appModules2Form,
|
||||
getDefaultAppForm,
|
||||
appForm2Modules,
|
||||
type EditFormType
|
||||
} from '@/utils/app';
|
||||
import { chatModelList } from '@/store/static';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import {
|
||||
ChatModelSystemTip,
|
||||
ChatModelLimitTip,
|
||||
welcomeTextTip
|
||||
} from '@/constants/flow/ModuleTemplate';
|
||||
import { AppModuleItemType, VariableItemType } from '@/types/app';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { streamFetch } from '@/api/fetch';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ChatBox, {
|
||||
getSpecialModule,
|
||||
type ComponentRef,
|
||||
type StartChatFnProps
|
||||
} from '@/components/ChatBox';
|
||||
import { addVariable } from '../VariableEditModal';
|
||||
import { KBSelectModal, KbParamsModal } from '../KBSelectModal';
|
||||
|
||||
const VariableEditModal = dynamic(() => import('../VariableEditModal'));
|
||||
|
||||
const Settings = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
const { appDetail, updateAppDetail, loadKbList, myKbList } = useUserStore();
|
||||
|
||||
const [editVariable, setEditVariable] = useState<VariableItemType>();
|
||||
|
||||
useQuery(['initkb', appId], () => loadKbList());
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
title: '警告',
|
||||
content: '保存后将会覆盖高级编排配置,请确保该应用未使用高级编排功能。'
|
||||
});
|
||||
const { register, setValue, getValues, reset, handleSubmit, control } = useForm<EditFormType>({
|
||||
defaultValues: getDefaultAppForm()
|
||||
});
|
||||
const {
|
||||
fields: variables,
|
||||
append: appendVariable,
|
||||
remove: removeVariable,
|
||||
replace: replaceVariables
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'variables'
|
||||
});
|
||||
const { fields: kbList, replace: replaceKbList } = useFieldArray({
|
||||
control,
|
||||
name: 'kb.list'
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenKbParams,
|
||||
onOpen: onOpenKbParams,
|
||||
onClose: onCloseKbParams
|
||||
} = useDisclosure();
|
||||
|
||||
const chatModelSelectList = useMemo(() => {
|
||||
return chatModelList.map((item) => ({
|
||||
value: item.model,
|
||||
label: `${item.name} (${formatPrice(item.price, 1000)} 元/1k tokens)`
|
||||
}));
|
||||
}, [refresh]);
|
||||
const tokenLimit = useMemo(() => {
|
||||
return (
|
||||
chatModelList.find((item) => item.model === getValues('chatModel.model'))?.contextMaxToken ||
|
||||
4000
|
||||
);
|
||||
}, [getValues, refresh]);
|
||||
const selectedKbList = useMemo(
|
||||
() => myKbList.filter((item) => kbList.find((kb) => kb.kbId === item._id)),
|
||||
[myKbList, kbList]
|
||||
);
|
||||
|
||||
const appModule2Form = useCallback(() => {
|
||||
const formVal = appModules2Form(appDetail.modules);
|
||||
reset(formVal);
|
||||
setRefresh((state) => !state);
|
||||
}, [appDetail.modules, reset]);
|
||||
|
||||
const { mutate: onSubmitSave, isLoading: isSaving } = useRequest({
|
||||
mutationFn: async (data: EditFormType) => {
|
||||
const modules = appForm2Modules(data);
|
||||
|
||||
await updateAppDetail(appDetail._id, {
|
||||
modules
|
||||
});
|
||||
},
|
||||
successToast: '保存成功',
|
||||
errorToast: '保存出现异常'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
appModule2Form();
|
||||
}, [appModule2Form]);
|
||||
|
||||
const BoxStyles: BoxProps = {
|
||||
bg: 'myWhite.300',
|
||||
px: 4,
|
||||
py: 3,
|
||||
borderRadius: 'lg',
|
||||
border: theme.borders.base
|
||||
};
|
||||
const BoxBtnStyles: BoxProps = {
|
||||
cursor: 'pointer',
|
||||
px: 3,
|
||||
py: '2px',
|
||||
borderRadius: 'md',
|
||||
_hover: {
|
||||
bg: 'myGray.200'
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
borderRight={'1.5px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
pt={4}
|
||||
>
|
||||
<Flex overflowY={'auto'} pr={4} justifyContent={'space-between'}>
|
||||
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
|
||||
应用配置
|
||||
<MyTooltip label={'仅包含基础功能,复杂 agent 功能请使用高级编排。'}>
|
||||
<QuestionOutlineIcon ml={2} fontSize={'md'} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Button
|
||||
isLoading={isSaving}
|
||||
fontSize={'sm'}
|
||||
onClick={openConfirm(handleSubmit((data) => onSubmitSave(data)))}
|
||||
>
|
||||
保存并预览
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'} my={4} pr={4} overflowY={'auto'}>
|
||||
{/* variable */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/variable.png'} objectFit={'contain'} w={'18px'} />
|
||||
<Box ml={2} flex={1}>
|
||||
变量
|
||||
</Box>
|
||||
<Flex {...BoxBtnStyles} onClick={() => setEditVariable(addVariable())}>
|
||||
+ 新增
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box
|
||||
mt={2}
|
||||
borderRadius={'lg'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
borderBottom="none"
|
||||
>
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>变量名</Th>
|
||||
<Th>变量 key</Th>
|
||||
<Th>必填</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{variables.map((item, index) => (
|
||||
<Tr key={item.id}>
|
||||
<Td>{item.label} </Td>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => setEditVariable(item)}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => removeVariable(index)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt={5} {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/AI.png'} w={'18px'} />
|
||||
<Box ml={2}>AI 配置</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box w={['60px', '100px']} flexShrink={0}>
|
||||
对话模型
|
||||
</Box>
|
||||
<MySelect
|
||||
width={['100%', '300px']}
|
||||
value={getValues('chatModel.model')}
|
||||
list={chatModelSelectList}
|
||||
onchange={(val: any) => {
|
||||
setValue('chatModel.model', val);
|
||||
const maxToken =
|
||||
chatModelList.find((item) => item.model === getValues('chatModel.model'))
|
||||
?.contextMaxToken || 4000;
|
||||
const token = maxToken / 2;
|
||||
setValue('chatModel.maxToken', token);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} my={10}>
|
||||
<Box w={['60px', '100px']} flexShrink={0}>
|
||||
温度
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '严谨', value: 0 },
|
||||
{ label: '发散', value: 10 }
|
||||
]}
|
||||
width={'95%'}
|
||||
min={0}
|
||||
max={10}
|
||||
value={getValues('chatModel.temperature')}
|
||||
onChange={(e) => {
|
||||
setValue('chatModel.temperature', e);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={12} mb={10}>
|
||||
<Box w={['60px', '100px']} flexShrink={0}>
|
||||
回复上限
|
||||
</Box>
|
||||
<Box flex={1} ml={'10px'}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '100', value: 100 },
|
||||
{ label: `${tokenLimit}`, value: tokenLimit }
|
||||
]}
|
||||
width={'95%'}
|
||||
min={100}
|
||||
max={tokenLimit}
|
||||
step={50}
|
||||
value={getValues('chatModel.maxToken')}
|
||||
onChange={(val) => {
|
||||
setValue('chatModel.maxToken', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={10} alignItems={'flex-start'}>
|
||||
<Box w={['60px', '100px']} flexShrink={0}>
|
||||
提示词
|
||||
<MyTooltip label={ChatModelSystemTip}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder={ChatModelSystemTip}
|
||||
borderColor={'myGray.100'}
|
||||
{...register('chatModel.systemPrompt')}
|
||||
></Textarea>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'flex-start'}>
|
||||
<Box w={['60px', '100px']} flexShrink={0}>
|
||||
限定词
|
||||
<MyTooltip label={ChatModelLimitTip}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Textarea
|
||||
rows={5}
|
||||
placeholder={ChatModelLimitTip}
|
||||
borderColor={'myGray.100'}
|
||||
{...register('chatModel.limitPrompt')}
|
||||
></Textarea>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* kb */}
|
||||
<Box mt={5} {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={1}>
|
||||
<Avatar src={'/imgs/module/db.png'} w={'18px'} />
|
||||
<Box ml={2}>知识库</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mr={3} {...BoxBtnStyles} onClick={onOpenKbSelect}>
|
||||
<SmallAddIcon />
|
||||
选择
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} {...BoxBtnStyles} onClick={onOpenKbParams}>
|
||||
<MyIcon name={'edit'} w={'14px'} mr={1} />
|
||||
参数
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex mt={1} color={'myGray.600'} fontSize={['sm', 'md']}>
|
||||
相似度: {getValues('kb.searchSimilarity')}, 单次搜索数量: {getValues('kb.searchLimit')},
|
||||
空搜索时拒绝回复: {getValues('kb.searchEmptyText') !== '' ? 'true' : 'false'}
|
||||
</Flex>
|
||||
<Grid templateColumns={['1fr', 'repeat(2,1fr)']} my={2} gridGap={[2, 4]}>
|
||||
{selectedKbList.map((item) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
p={2}
|
||||
bg={'white'}
|
||||
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
|
||||
borderRadius={'md'}
|
||||
border={theme.borders.base}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'18px'} mr={1} />
|
||||
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* welcome */}
|
||||
<Box mt={5} {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={'/imgs/module/userGuide.png'} w={'18px'} />
|
||||
<Box mx={2}>对话开场白</Box>
|
||||
<MyTooltip label={welcomeTextTip}>
|
||||
<QuestionOutlineIcon />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
mt={2}
|
||||
rows={5}
|
||||
placeholder={welcomeTextTip}
|
||||
borderColor={'myGray.100'}
|
||||
{...register('guide.welcome.text')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ConfirmChild />
|
||||
{editVariable && (
|
||||
<VariableEditModal
|
||||
defaultVariable={editVariable}
|
||||
onClose={() => setEditVariable(undefined)}
|
||||
onSubmit={({ variable }) => {
|
||||
const record = variables.find((item) => item.id === variable.id);
|
||||
if (record) {
|
||||
replaceVariables(
|
||||
variables.map((item) => (item.id === variable.id ? variable : item))
|
||||
);
|
||||
} else {
|
||||
appendVariable(variable);
|
||||
}
|
||||
|
||||
setEditVariable(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOpenKbSelect && (
|
||||
<KBSelectModal
|
||||
kbList={myKbList}
|
||||
activeKbs={selectedKbList.map((item) => ({ kbId: item._id }))}
|
||||
onClose={onCloseKbSelect}
|
||||
onChange={replaceKbList}
|
||||
/>
|
||||
)}
|
||||
{isOpenKbParams && (
|
||||
<KbParamsModal
|
||||
searchEmptyText={getValues('kb.searchEmptyText')}
|
||||
searchLimit={getValues('kb.searchLimit')}
|
||||
searchSimilarity={getValues('kb.searchSimilarity')}
|
||||
onClose={onCloseKbParams}
|
||||
onChange={({ searchEmptyText, searchLimit, searchSimilarity }) => {
|
||||
setValue('kb.searchEmptyText', searchEmptyText);
|
||||
setValue('kb.searchLimit', searchLimit);
|
||||
setValue('kb.searchSimilarity', searchSimilarity);
|
||||
setRefresh((state) => !state);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatTest = ({ appId }: { appId: string }) => {
|
||||
const { appDetail } = useUserStore();
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const [modules, setModules] = useState<AppModuleItemType[]>([]);
|
||||
|
||||
const startChat = useCallback(
|
||||
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
const historyMaxLen =
|
||||
modules
|
||||
?.find((item) => item.flowType === FlowModuleTypeEnum.historyNode)
|
||||
?.inputs?.find((item) => item.key === 'maxContext')?.value || 0;
|
||||
const history = messages.slice(-historyMaxLen - 2, -2);
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText, rawSearch } = await streamFetch({
|
||||
url: '/api/chat/chatTest',
|
||||
data: {
|
||||
history,
|
||||
prompt: messages[messages.length - 2].content,
|
||||
modules,
|
||||
variables,
|
||||
appId,
|
||||
appName: `调试-${appDetail.name}`
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortSignal: controller
|
||||
});
|
||||
|
||||
return { responseText, rawSearch };
|
||||
},
|
||||
[modules, appId, appDetail.name]
|
||||
);
|
||||
|
||||
const resetChatBox = useCallback(() => {
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const formVal = appModules2Form(appDetail.modules);
|
||||
setModules(appForm2Modules(formVal));
|
||||
resetChatBox();
|
||||
}, [appDetail, resetChatBox]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} pl={4} py={4}>
|
||||
<Flex>
|
||||
<Box fontSize={['md', 'xl']} fontWeight={'bold'} flex={1}>
|
||||
调试预览
|
||||
</Box>
|
||||
<MyTooltip label={'重置'}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'clearLight'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
resetChatBox();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appAvatar={appDetail.avatar}
|
||||
{...getSpecialModule(modules)}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const BasicEdit = ({ appId }: { appId: string }) => {
|
||||
return (
|
||||
<Grid gridTemplateColumns={['1fr', '550px 1fr']} h={'100%'}>
|
||||
<Settings appId={appId} />
|
||||
<ChatTest appId={appId} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicEdit;
|
@@ -192,7 +192,7 @@ const TokenUsage = ({ appId }: { appId: string }) => {
|
||||
}, [screenWidth]);
|
||||
|
||||
return (
|
||||
<Box ref={Dom} w={'100%'} flex={'1 0 0'} h={'100%'} position={'relative'}>
|
||||
<Box ref={Dom} w={'100%'} flex={'1 0 0'} h={'100%'} minH={'150px'} position={'relative'}>
|
||||
<Loading fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
|
@@ -1,12 +1,73 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import { Flex, Box, Button, useTheme, useDisclosure, Grid } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import Divider from '../modules/Divider';
|
||||
import Container from '../modules/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import KBSelect from '../Plugins/KBSelect';
|
||||
import { KBSelectModal } from '../../../KBSelectModal';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const KBSelect = ({
|
||||
activeKbs = [],
|
||||
onChange
|
||||
}: {
|
||||
activeKbs: SelectedKbType;
|
||||
onChange: (e: SelectedKbType) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { myKbList, loadKbList } = useUserStore();
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const showKbList = useMemo(
|
||||
() => myKbList.filter((item) => activeKbs.find((kb) => kb.kbId === item._id)),
|
||||
[myKbList, activeKbs]
|
||||
);
|
||||
|
||||
useQuery(['initkb'], 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>
|
||||
{isOpenKbSelect && (
|
||||
<KBSelectModal
|
||||
kbList={myKbList}
|
||||
activeKbs={activeKbs}
|
||||
onChange={onChange}
|
||||
onClose={onCloseKbSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeKbSearch = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
@@ -20,9 +81,9 @@ const NodeKbSearch = ({
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
kb_ids: ({ key, value }) => (
|
||||
kbList: ({ key, value }) => (
|
||||
<KBSelect
|
||||
relatedKbs={value}
|
||||
activeKbs={value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
|
@@ -7,12 +7,8 @@ import { FlowModuleItemType } from '@/types/flow';
|
||||
import Container from '../modules/Container';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const welcomePlaceholder =
|
||||
'每次对话开始前,发送一个初始内容。支持标准 Markdown 语法,可使用的额外标记:\n[快捷按键]: 用户点击后可以直接发送该问题';
|
||||
import { welcomeTextTip } from '@/constants/flow/ModuleTemplate';
|
||||
|
||||
const NodeUserGuide = ({
|
||||
data: { inputs, outputs, onChangeNode, ...props }
|
||||
@@ -30,7 +26,7 @@ const NodeUserGuide = ({
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
|
||||
<Box>开场白</Box>
|
||||
<MyTooltip label={welcomePlaceholder}>
|
||||
<MyTooltip label={welcomeTextTip}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
@@ -40,7 +36,7 @@ const NodeUserGuide = ({
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText}
|
||||
bg={'myWhite.500'}
|
||||
placeholder={welcomePlaceholder}
|
||||
placeholder={welcomeTextTip}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId: props.moduleId,
|
||||
|
@@ -45,7 +45,7 @@ const VariableTypeList = [
|
||||
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
|
||||
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
|
||||
];
|
||||
const defaultVariable: VariableItemType = {
|
||||
export const defaultVariable: VariableItemType = {
|
||||
id: nanoid(),
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
@@ -66,10 +66,6 @@ const NodeUserGuide = ({
|
||||
?.value as VariableItemType[]) || [],
|
||||
[inputs]
|
||||
);
|
||||
const welcomeText = useMemo(
|
||||
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
@@ -1,143 +0,0 @@
|
||||
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;
|
@@ -17,10 +17,11 @@ import {
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { delModelById, putAppById } from '@/api/app';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/file';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const InfoModal = ({
|
||||
@@ -30,9 +31,11 @@ const InfoModal = ({
|
||||
}: {
|
||||
defaultApp: AppSchema;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const { updateAppDetail } = useUserStore();
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
@@ -47,31 +50,30 @@ const InfoModal = ({
|
||||
} = useForm({
|
||||
defaultValues: defaultApp
|
||||
});
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
// 提交保存模型修改
|
||||
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
|
||||
});
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setBtnLoading(false);
|
||||
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
|
||||
mutationFn: async (data: AppSchema) => {
|
||||
await updateAppDetail(data._id, {
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
intro: data.intro,
|
||||
chat: data.chat,
|
||||
share: data.share
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
onSuccess() {
|
||||
onSuccess && onSuccess();
|
||||
onClose();
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
errorToast: '更新失败'
|
||||
});
|
||||
|
||||
// 提交保存表单失败
|
||||
const saveSubmitError = useCallback(() => {
|
||||
// deep search message
|
||||
@@ -91,7 +93,7 @@ const InfoModal = ({
|
||||
}, [errors, toast]);
|
||||
|
||||
const saveUpdateModel = useCallback(
|
||||
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
|
||||
() => handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)(),
|
||||
[handleSubmit, saveSubmitError, saveSubmitSuccess]
|
||||
);
|
||||
|
||||
@@ -165,22 +167,7 @@ const InfoModal = ({
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={btnLoading}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await saveUpdateModel();
|
||||
onSuccess();
|
||||
onClose();
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button isLoading={btnLoading} onClick={saveUpdateModel}>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
214
client/src/pages/app/detail/components/KBSelectModal.tsx
Normal file
214
client/src/pages/app/detail/components/KBSelectModal.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
useTheme,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { KbListItemType } from '@/types/plugin';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
import MySlider from '@/components/Slider';
|
||||
|
||||
export type KbParamsType = {
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
};
|
||||
|
||||
export const KBSelectModal = ({
|
||||
kbList,
|
||||
activeKbs = [],
|
||||
onChange,
|
||||
onClose
|
||||
}: {
|
||||
kbList: KbListItemType[];
|
||||
activeKbs: SelectedKbType;
|
||||
onChange: (e: SelectedKbType) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [selectedKbList, setSelectedKbList] = useState<SelectedKbType>(activeKbs);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
w={'800px'}
|
||||
maxW={'90vw'}
|
||||
h={['90vh', 'auto']}
|
||||
>
|
||||
<ModalHeader>关联的知识库({selectedKbList.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) =>
|
||||
(() => {
|
||||
const selected = !!selectedKbList.find((kb) => kb.kbId === item._id);
|
||||
const active = !!activeKbs.find((kb) => kb.kbId === item._id);
|
||||
return (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
order={active ? 0 : 1}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
{...(selected
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
if (selected) {
|
||||
setSelectedKbList((state) => state.filter((kb) => kb.kbId !== item._id));
|
||||
} else {
|
||||
setSelectedKbList((state) => [...state, { kbId: item._id }]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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={() => {
|
||||
onClose();
|
||||
onChange(selectedKbList);
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const KbParamsModal = ({
|
||||
searchEmptyText,
|
||||
searchLimit,
|
||||
searchSimilarity,
|
||||
onClose,
|
||||
onChange
|
||||
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
|
||||
defaultValues: {
|
||||
searchEmptyText,
|
||||
searchLimit,
|
||||
searchSimilarity
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
|
||||
<ModalHeader>搜索参数调整</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex pt={3} pb={5}>
|
||||
<Box flex={'0 0 100px'}>
|
||||
相似度
|
||||
<MyTooltip label={'高相似度推荐0.8及以上。'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '1', value: 1 }
|
||||
]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getValues('searchSimilarity')}
|
||||
onChange={(val) => {
|
||||
setValue('searchSimilarity', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</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={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
handleSubmit(onChange)();
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -7,12 +7,13 @@ import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { delModelById } from '@/api/app';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import TotalUsage from './Charts/TotalUsage';
|
||||
import BasicEdit from './BasicEdit';
|
||||
|
||||
const InfoModal = dynamic(() => import('./InfoModal'));
|
||||
|
||||
@@ -54,98 +55,100 @@ const OverView = ({ appId }: { appId: string }) => {
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} position={'relative'}>
|
||||
<Box w={'100%'} pt={[0, 7]} px={[3, 5, 8]}>
|
||||
<Grid gridTemplateColumns={['1fr', 'repeat(2,1fr)']} gridGap={[2, 4, 6]}>
|
||||
<Box>
|
||||
<Box mb={2} fontSize={['md', 'xl']}>
|
||||
基本信息
|
||||
</Box>
|
||||
<Box
|
||||
border={theme.borders.base}
|
||||
borderRadius={'lg'}
|
||||
px={5}
|
||||
py={4}
|
||||
bg={'myBlue.100'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex alignItems={'center'} py={2}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
_hover={{
|
||||
bg: 'myGray.100',
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={openConfirm(handleDelModel)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box className={'textEllipsis3'} py={3} wordBreak={'break-all'} color={'myGray.600'}>
|
||||
{appDetail.intro || '快来给应用一个介绍~'}
|
||||
</Box>
|
||||
<Flex>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
>
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
mx={3}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'shareLight'} w={'16px'} />}
|
||||
onClick={() => {
|
||||
router.replace({
|
||||
query: {
|
||||
appId,
|
||||
currentTab: 'share'
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
分享
|
||||
</Button>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'settingLight'} w={'16px'} />}
|
||||
onClick={() => setSettingAppInfo(appDetail)}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Grid
|
||||
gridTemplateColumns={['1fr', 'repeat(2,1fr)']}
|
||||
gridGap={[2, 4, 6]}
|
||||
pt={[0, 7]}
|
||||
px={[3, 5, 8]}
|
||||
>
|
||||
<Box>
|
||||
<Box mb={2} fontSize={['md', 'xl']}>
|
||||
基本信息
|
||||
</Box>
|
||||
<Flex flexDirection={'column'}>
|
||||
<Box mb={2} fontSize={['md', 'xl']}>
|
||||
近 14 日消费
|
||||
<Box
|
||||
border={theme.borders.base}
|
||||
borderRadius={'lg'}
|
||||
px={5}
|
||||
py={4}
|
||||
bg={'myBlue.100'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex alignItems={'center'} py={2}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
_hover={{
|
||||
bg: 'myGray.100',
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={openConfirm(handleDelModel)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box className={'textEllipsis3'} py={3} wordBreak={'break-all'} color={'myGray.600'}>
|
||||
{appDetail.intro || '快来给应用一个介绍~'}
|
||||
</Box>
|
||||
<TotalUsage appId={appId} />
|
||||
</Flex>
|
||||
</Grid>
|
||||
<Flex>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
>
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
mx={3}
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'shareLight'} w={'16px'} />}
|
||||
onClick={() => {
|
||||
router.replace({
|
||||
query: {
|
||||
appId,
|
||||
currentTab: 'share'
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
分享
|
||||
</Button>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'base'}
|
||||
leftIcon={<MyIcon name={'settingLight'} w={'16px'} />}
|
||||
onClick={() => setSettingAppInfo(appDetail)}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex flexDirection={'column'}>
|
||||
<Box mb={2} fontSize={['md', 'xl']}>
|
||||
近 14 日消费
|
||||
</Box>
|
||||
<TotalUsage appId={appId} />
|
||||
</Flex>
|
||||
</Grid>
|
||||
|
||||
<Box flex={'1 0 0'} h={0} mt={4} borderTop={theme.borders.base} px={[3, 5, 8]}>
|
||||
<BasicEdit appId={appId} />
|
||||
</Box>
|
||||
|
||||
{settingAppInfo && (
|
||||
<InfoModal
|
||||
defaultApp={settingAppInfo}
|
||||
onClose={() => setSettingAppInfo(undefined)}
|
||||
onSuccess={refetch}
|
||||
/>
|
||||
<InfoModal defaultApp={settingAppInfo} onClose={() => setSettingAppInfo(undefined)} />
|
||||
)}
|
||||
|
||||
<ConfirmChild />
|
||||
<Loading fixed={false} />
|
||||
</Flex>
|
||||
|
214
client/src/pages/app/detail/components/VariableEditModal.tsx
Normal file
214
client/src/pages/app/detail/components/VariableEditModal.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
Grid,
|
||||
FormControl,
|
||||
useTheme
|
||||
} from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { VariableInputEnum } from '@/constants/app';
|
||||
import type { VariableItemType } from '@/types/app';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const VariableTypeList = [
|
||||
{ label: '文本', icon: 'settingLight', key: VariableInputEnum.input },
|
||||
{ label: '下拉单选', icon: 'settingLight', key: VariableInputEnum.select }
|
||||
];
|
||||
|
||||
export type VariableFormType = {
|
||||
variable: VariableItemType;
|
||||
};
|
||||
|
||||
const VariableEditModal = ({
|
||||
defaultVariable,
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
defaultVariable: VariableItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: VariableFormType) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const { reset, getValues, setValue, register, control, handleSubmit } = useForm<VariableFormType>(
|
||||
{
|
||||
defaultValues: {
|
||||
variable: defaultVariable
|
||||
}
|
||||
}
|
||||
);
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
remove: removeEnums
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'variable.enums'
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'Min(400px,90vw)'}>
|
||||
<ModalHeader display={'flex'}>
|
||||
<MyIcon name={'variable'} mr={2} w={'24px'} color={'#FF8A4C'} />
|
||||
变量设置
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box w={'70px'}>必填</Box>
|
||||
<Switch {...register('variable.required')} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={'80px'}>变量名</Box>
|
||||
<Input {...register('variable.label', { required: '变量名不能为空' })} />
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={'80px'}>变量 key</Box>
|
||||
<Input {...register('variable.key', { required: '变量 key 不能为空' })} />
|
||||
</Flex>
|
||||
|
||||
<Box mt={5} mb={2}>
|
||||
字段类型
|
||||
</Box>
|
||||
<Grid gridTemplateColumns={'repeat(2,130px)'} gridGap={4}>
|
||||
{VariableTypeList.map((item) => (
|
||||
<Flex
|
||||
key={item.key}
|
||||
px={4}
|
||||
py={1}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
{...(item.key === getValues('variable.type')
|
||||
? {
|
||||
bg: 'myWhite.600'
|
||||
}
|
||||
: {
|
||||
_hover: {
|
||||
boxShadow: 'md'
|
||||
},
|
||||
onClick: () => {
|
||||
setValue('variable.type', item.key);
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
})}
|
||||
>
|
||||
<MyIcon name={item.icon as any} w={'16px'} />
|
||||
<Box ml={3}>{item.label}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{getValues('variable.type') === VariableInputEnum.input && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
最大长度
|
||||
</Box>
|
||||
<Box>
|
||||
<NumberInput max={100} min={1} step={1} position={'relative'}>
|
||||
<NumberInputField
|
||||
{...register('variable.maxLen', {
|
||||
min: 1,
|
||||
max: 100,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
max={100}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{getValues('variable.type') === VariableInputEnum.select && (
|
||||
<>
|
||||
<Box mt={5} mb={2}>
|
||||
选项
|
||||
</Box>
|
||||
<Box>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} mb={2} alignItems={'center'}>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...register(`variable.enums.${i}.value`, {
|
||||
required: '选项内容不能为空'
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'lg'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'solid'}
|
||||
w={'100%'}
|
||||
textAlign={'left'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
bg={'myGray.100 !important'}
|
||||
onClick={() => appendEnums({ value: '' })}
|
||||
>
|
||||
添加选项
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>确认</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(VariableEditModal);
|
||||
|
||||
export const defaultVariable: VariableItemType = {
|
||||
id: nanoid(),
|
||||
key: 'key',
|
||||
label: 'label',
|
||||
type: VariableInputEnum.input,
|
||||
required: true,
|
||||
maxLen: 50,
|
||||
enums: [{ value: '' }]
|
||||
};
|
||||
export const addVariable = () => {
|
||||
const newVariable = { ...defaultVariable, id: nanoid() };
|
||||
return newVariable;
|
||||
};
|
@@ -54,7 +54,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
{ label: '概览', id: TabEnum.overview, icon: 'overviewLight' },
|
||||
{ label: '基础', id: TabEnum.overview, icon: 'overviewLight' },
|
||||
{ label: '高级编排', id: TabEnum.settings, icon: 'settingLight' },
|
||||
{ label: '链接分享', id: TabEnum.share, icon: 'shareLight' },
|
||||
{ label: 'API访问', id: TabEnum.API, icon: 'apiLight' },
|
||||
@@ -150,7 +150,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
<Tabs
|
||||
mx={'auto'}
|
||||
mt={2}
|
||||
w={'300px'}
|
||||
w={'100%'}
|
||||
list={tabList}
|
||||
size={'sm'}
|
||||
activeId={currentTab}
|
||||
|
@@ -79,7 +79,7 @@ const ChatHistorySlider = ({
|
||||
cursor={appId ? 'pointer' : 'default'}
|
||||
onClick={() =>
|
||||
appId &&
|
||||
router.push({
|
||||
router.replace({
|
||||
pathname: '/app/detail',
|
||||
query: { appId }
|
||||
})
|
||||
|
@@ -21,7 +21,7 @@ const SliderApps = ({ appId }: { appId: string }) => {
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.200' }}
|
||||
onClick={() => router.push('/app/list')}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
|
@@ -239,7 +239,7 @@ const Chat = () => {
|
||||
top: item.top
|
||||
}))}
|
||||
onChangeChat={(chatId) => {
|
||||
router.push({
|
||||
router.replace({
|
||||
query: {
|
||||
chatId: chatId || '',
|
||||
appId
|
||||
|
@@ -2,11 +2,11 @@ import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import type { UserType, UserUpdateParams } from '@/types/user';
|
||||
import { getMyModels, getModelById } from '@/api/app';
|
||||
import { getMyModels, getModelById, putAppById } from '@/api/app';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { getTokenLogin } from '@/api/user';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
import { AppListItemType } from '@/types/app';
|
||||
import { AppListItemType, AppUpdateParams } from '@/types/app';
|
||||
import type { KbItemType, KbListItemType } from '@/types/plugin';
|
||||
import { getKbList, getKbById } from '@/api/plugins/kb';
|
||||
import { defaultKbDetail } from '@/constants/kb';
|
||||
@@ -22,6 +22,7 @@ type State = {
|
||||
loadMyModels: () => Promise<null>;
|
||||
appDetail: AppSchema;
|
||||
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
|
||||
updateAppDetail(appId: string, data: AppUpdateParams): Promise<void>;
|
||||
clearAppModules(): void;
|
||||
// kb
|
||||
myKbList: KbListItemType[];
|
||||
@@ -79,6 +80,15 @@ export const useUserStore = create<State>()(
|
||||
});
|
||||
return res;
|
||||
},
|
||||
async updateAppDetail(appId: string, data: AppUpdateParams) {
|
||||
await putAppById(appId, data);
|
||||
set((state) => {
|
||||
state.appDetail = {
|
||||
...state.appDetail,
|
||||
...data
|
||||
};
|
||||
});
|
||||
},
|
||||
clearAppModules() {
|
||||
set((state) => {
|
||||
state.appDetail = {
|
||||
|
2
client/src/types/plugin.d.ts
vendored
2
client/src/types/plugin.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
import type { kbSchema } from './mongoSchema';
|
||||
|
||||
export type SelectedKbType = { kbId: string }[];
|
||||
|
||||
export type KbListItemType = {
|
||||
_id: string;
|
||||
avatar: string;
|
||||
|
611
client/src/utils/app.ts
Normal file
611
client/src/utils/app.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
import type { AppModuleItemType, VariableItemType } from '@/types/app';
|
||||
import { chatModelList, vectorModelList } from '@/store/static';
|
||||
import { FlowModuleTypeEnum } from '@/constants/flow';
|
||||
import { FlowInputItemType } from '@/types/flow';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
import {
|
||||
VariableModule,
|
||||
UserGuideModule,
|
||||
ChatModule,
|
||||
HistoryModule,
|
||||
UserInputModule,
|
||||
KBSearchModule,
|
||||
AnswerModule
|
||||
} from '@/constants/flow/ModuleTemplate';
|
||||
import { rawSearchKey } from '@/constants/chat';
|
||||
|
||||
export type EditFormType = {
|
||||
chatModel: {
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
frequency: number;
|
||||
presence: number;
|
||||
};
|
||||
kb: {
|
||||
list: SelectedKbType;
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
};
|
||||
guide: {
|
||||
welcome: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
variables: VariableItemType[];
|
||||
};
|
||||
export const getDefaultAppForm = (): EditFormType => {
|
||||
const defaultChatModel = chatModelList[0];
|
||||
const defaultVectorModel = vectorModelList[0];
|
||||
return {
|
||||
chatModel: {
|
||||
model: defaultChatModel.model,
|
||||
systemPrompt: '',
|
||||
limitPrompt: '',
|
||||
temperature: 0,
|
||||
maxToken: defaultChatModel.contextMaxToken / 2,
|
||||
frequency: 0.5,
|
||||
presence: -0.5
|
||||
},
|
||||
kb: {
|
||||
list: [],
|
||||
searchSimilarity: 0.8,
|
||||
searchLimit: 5,
|
||||
searchEmptyText: ''
|
||||
},
|
||||
guide: {
|
||||
welcome: {
|
||||
text: ''
|
||||
}
|
||||
},
|
||||
variables: []
|
||||
};
|
||||
};
|
||||
|
||||
export const appModules2Form = (modules: AppModuleItemType[]) => {
|
||||
const defaultAppForm = getDefaultAppForm();
|
||||
const updateVal = ({
|
||||
formKey,
|
||||
inputs,
|
||||
key
|
||||
}: {
|
||||
formKey: string;
|
||||
inputs: FlowInputItemType[];
|
||||
key: string;
|
||||
}) => {
|
||||
const propertyPath = formKey.split('.');
|
||||
let currentObj: any = defaultAppForm;
|
||||
for (let i = 0; i < propertyPath.length - 1; i++) {
|
||||
currentObj = currentObj[propertyPath[i]];
|
||||
}
|
||||
|
||||
const val =
|
||||
inputs.find((item) => item.key === key)?.value ||
|
||||
currentObj[propertyPath[propertyPath.length - 1]];
|
||||
|
||||
currentObj[propertyPath[propertyPath.length - 1]] = val;
|
||||
};
|
||||
|
||||
modules.forEach((module) => {
|
||||
if (module.flowType === FlowModuleTypeEnum.chatNode) {
|
||||
updateVal({
|
||||
formKey: 'chatModel.model',
|
||||
inputs: module.inputs,
|
||||
key: 'model'
|
||||
});
|
||||
updateVal({
|
||||
formKey: 'chatModel.temperature',
|
||||
inputs: module.inputs,
|
||||
key: 'temperature'
|
||||
});
|
||||
updateVal({
|
||||
formKey: 'chatModel.maxToken',
|
||||
inputs: module.inputs,
|
||||
key: 'maxToken'
|
||||
});
|
||||
updateVal({
|
||||
formKey: 'chatModel.systemPrompt',
|
||||
inputs: module.inputs,
|
||||
key: 'systemPrompt'
|
||||
});
|
||||
updateVal({
|
||||
formKey: 'chatModel.limitPrompt',
|
||||
inputs: module.inputs,
|
||||
key: 'limitPrompt'
|
||||
});
|
||||
} else if (module.flowType === FlowModuleTypeEnum.kbSearchNode) {
|
||||
updateVal({
|
||||
formKey: 'kb.list',
|
||||
inputs: module.inputs,
|
||||
key: 'kbList'
|
||||
});
|
||||
updateVal({
|
||||
formKey: 'kb.searchSimilarity',
|
||||
inputs: module.inputs,
|
||||
key: 'similarity'
|
||||
});
|
||||
updateVal({
|
||||
formKey: 'kb.searchLimit',
|
||||
inputs: module.inputs,
|
||||
key: 'limit'
|
||||
});
|
||||
// empty text
|
||||
const emptyOutputs = module.outputs.find((item) => item.key === 'isEmpty')?.targets || [];
|
||||
const emptyOutput = emptyOutputs[0];
|
||||
if (emptyOutput) {
|
||||
const target = modules.find((item) => item.moduleId === emptyOutput.moduleId);
|
||||
defaultAppForm.kb.searchEmptyText =
|
||||
target?.inputs?.find((item) => item.key === 'answerText')?.value || '';
|
||||
}
|
||||
} else if (module.flowType === FlowModuleTypeEnum.userGuide) {
|
||||
const val =
|
||||
module.inputs.find((item) => item.key === SystemInputEnum.welcomeText)?.value || '';
|
||||
if (val) {
|
||||
defaultAppForm.guide.welcome = {
|
||||
text: val
|
||||
};
|
||||
}
|
||||
} else if (module.flowType === FlowModuleTypeEnum.variable) {
|
||||
defaultAppForm.variables =
|
||||
module.inputs.find((item) => item.key === SystemInputEnum.variables)?.value || [];
|
||||
}
|
||||
});
|
||||
|
||||
return defaultAppForm;
|
||||
};
|
||||
|
||||
const chatModelInput = (formData: EditFormType) => [
|
||||
{
|
||||
key: 'model',
|
||||
type: 'custom',
|
||||
label: '对话模型',
|
||||
value: formData.chatModel.model,
|
||||
list: chatModelList.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.model
|
||||
})),
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
type: 'custom',
|
||||
label: '温度',
|
||||
value: formData.chatModel.temperature,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
markList: [
|
||||
{
|
||||
label: '严谨',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: '发散',
|
||||
value: 10
|
||||
}
|
||||
],
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'maxToken',
|
||||
type: 'custom',
|
||||
label: '回复上限',
|
||||
value: formData.chatModel.maxToken,
|
||||
min: 100,
|
||||
max: 16000,
|
||||
step: 50,
|
||||
markList: [
|
||||
{
|
||||
label: '0',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: '16000',
|
||||
value: 16000
|
||||
}
|
||||
],
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'systemPrompt',
|
||||
type: 'textarea',
|
||||
label: '系统提示词',
|
||||
description:
|
||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。',
|
||||
placeholder:
|
||||
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。',
|
||||
value: formData.chatModel.systemPrompt,
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'limitPrompt',
|
||||
type: 'textarea',
|
||||
label: '限定词',
|
||||
description:
|
||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"',
|
||||
placeholder:
|
||||
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"',
|
||||
value: formData.chatModel.limitPrompt,
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'quotePrompt',
|
||||
type: 'target',
|
||||
label: '引用内容',
|
||||
connected: formData.kb.list.length > 0
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'target',
|
||||
label: '聊天记录',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'userChatInput',
|
||||
type: 'target',
|
||||
label: '用户问题',
|
||||
connected: true
|
||||
}
|
||||
];
|
||||
const welcomeTemplate = (formData: EditFormType) =>
|
||||
formData.guide?.welcome?.text
|
||||
? [
|
||||
{
|
||||
...UserGuideModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'welcomeText',
|
||||
type: 'input',
|
||||
label: '开场白',
|
||||
value: formData.guide.welcome.text,
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
position: {
|
||||
x: 447.98520778293346,
|
||||
y: 721.4016845336229
|
||||
},
|
||||
moduleId: 'v7lq0x'
|
||||
}
|
||||
]
|
||||
: [];
|
||||
const variableTemplate = (formData: EditFormType) =>
|
||||
formData.variables.length > 0
|
||||
? [
|
||||
{
|
||||
...VariableModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'variables',
|
||||
type: 'systemInput',
|
||||
label: '变量输入',
|
||||
value: formData.variables,
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
position: {
|
||||
x: 444.0369195277651,
|
||||
y: 1008.5185781784537
|
||||
},
|
||||
moduleId: '7blchb'
|
||||
}
|
||||
]
|
||||
: [];
|
||||
const simpleChatTemplate = (formData: EditFormType) => [
|
||||
{
|
||||
...UserInputModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
type: 'systemInput',
|
||||
label: '用户问题',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
label: '用户问题',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'userChatInput'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 464.32198615344566,
|
||||
y: 1602.2698463081606
|
||||
},
|
||||
moduleId: '7z5g5h'
|
||||
},
|
||||
{
|
||||
...HistoryModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 10,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'hidden',
|
||||
label: '聊天记录',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
label: '聊天记录',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'xj0c9p'
|
||||
},
|
||||
{
|
||||
...ChatModule,
|
||||
inputs: chatModelInput(formData),
|
||||
outputs: [
|
||||
{
|
||||
key: 'answerText',
|
||||
label: '模型回复',
|
||||
description: '直接响应,无需配置',
|
||||
type: 'hidden',
|
||||
targets: []
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 981.9682828103937,
|
||||
y: 890.014595014464
|
||||
},
|
||||
moduleId: 'chatModule'
|
||||
}
|
||||
];
|
||||
const kbTemplate = (formData: EditFormType) => [
|
||||
{
|
||||
...UserInputModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
type: 'systemInput',
|
||||
label: '用户问题',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
label: '用户问题',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'userChatInput'
|
||||
},
|
||||
{
|
||||
moduleId: 'q9v14m',
|
||||
key: 'userChatInput'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 464.32198615344566,
|
||||
y: 1602.2698463081606
|
||||
},
|
||||
moduleId: '7z5g5h'
|
||||
},
|
||||
{
|
||||
...HistoryModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
type: 'numberInput',
|
||||
label: '最长记录数',
|
||||
value: 10,
|
||||
min: 0,
|
||||
max: 50,
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'hidden',
|
||||
label: '聊天记录',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
label: '聊天记录',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'xj0c9p'
|
||||
},
|
||||
{
|
||||
...KBSearchModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'kbList',
|
||||
type: 'custom',
|
||||
label: '关联的知识库',
|
||||
value: formData.kb.list,
|
||||
list: [],
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'similarity',
|
||||
type: 'custom',
|
||||
label: '相似度',
|
||||
value: formData.kb.searchSimilarity,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
markList: [
|
||||
{
|
||||
label: '0',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: '1',
|
||||
value: 1
|
||||
}
|
||||
],
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'limit',
|
||||
type: 'custom',
|
||||
label: '单次搜索上限',
|
||||
description: '最多取 n 条记录作为本次问题引用',
|
||||
value: formData.kb.searchLimit,
|
||||
min: 1,
|
||||
max: 20,
|
||||
step: 1,
|
||||
markList: [
|
||||
{
|
||||
label: '1',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '20',
|
||||
value: 20
|
||||
}
|
||||
],
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'switch',
|
||||
type: 'target',
|
||||
label: '触发器',
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'userChatInput',
|
||||
type: 'target',
|
||||
label: '用户问题',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'isEmpty',
|
||||
label: '搜索结果为空',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'emptyText',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'quotePrompt',
|
||||
label: '引用内容',
|
||||
description: '搜索结果为空时不返回',
|
||||
type: 'source',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'quotePrompt'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 956.0838440206068,
|
||||
y: 887.462827870246
|
||||
},
|
||||
moduleId: 'q9v14m'
|
||||
},
|
||||
...(formData.kb.searchEmptyText
|
||||
? [
|
||||
{
|
||||
...AnswerModule,
|
||||
inputs: [
|
||||
{
|
||||
key: 'switch',
|
||||
type: 'target',
|
||||
label: '触发器',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'answerText',
|
||||
value: formData.kb.searchEmptyText,
|
||||
type: 'input',
|
||||
label: '回复的内容',
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
position: {
|
||||
x: 1570.7651822907549,
|
||||
y: 637.8753731306779
|
||||
},
|
||||
moduleId: 'emptyText'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
...ChatModule,
|
||||
inputs: chatModelInput(formData),
|
||||
outputs: [
|
||||
{
|
||||
key: 'answerText',
|
||||
label: '模型回复',
|
||||
description: '直接响应,无需配置',
|
||||
type: 'hidden',
|
||||
targets: []
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 1551.71405495818,
|
||||
y: 977.4911578918461
|
||||
},
|
||||
moduleId: 'chatModule'
|
||||
}
|
||||
];
|
||||
|
||||
export const appForm2Modules = (formData: EditFormType) => {
|
||||
const modules = [
|
||||
...welcomeTemplate(formData),
|
||||
...variableTemplate(formData),
|
||||
...(formData.kb.list.length > 0 ? kbTemplate(formData) : simpleChatTemplate(formData))
|
||||
];
|
||||
|
||||
return modules as AppModuleItemType[];
|
||||
};
|
Reference in New Issue
Block a user