feat: http modules

This commit is contained in:
archer
2023-08-03 11:09:57 +08:00
parent b7934ecc27
commit 952da2a06e
31 changed files with 851 additions and 176 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -9,7 +9,9 @@
"Confirm Save App Tip": "After saving, the advanced orchestration configuration will be overwritten. Make sure that the application does not use advanced orchestration.",
"Connection is invalid": "Connecting is invalid",
"Connection type is different": "Connection type is different",
"My Apps": "My Apps"
"Input Field Settings": "Input Field Settings",
"My Apps": "My Apps",
"Output Field Settings": "Output Field Settings"
},
"chat": {
"Confirm to clear history": "Confirm to clear history?",
@@ -39,28 +41,28 @@
"Tools": "Tools"
},
"user": {
"Account": "Account",
"Application Name": "Application Name",
"Avatar": "Avatar",
"Balance": "Balance",
"Bill Detail": "Bill Detail",
"Change": "Change",
"Notice": "Notice",
"Old password is error": "Old password is error",
"OpenAI Account Setting": "OpenAI Account Setting",
"Password": "Password",
"Pay": "Pay",
"Personal Information": "Personal",
"Recharge Record": "Recharge",
"Replace": "Replace",
"Set OpenAI Account Failed": "Set OpenAI account failed",
"Sign Out": "Sign Out",
"Source": "Source",
"Time": "Time",
"Total Amount": "Total Amount",
"Update Password": "Update Password",
"Update password failed": "Update password failed",
"Update password succseful": "Update password succseful",
"Personal Information": "Personal",
"Usage Record": "Usage",
"Recharge Record": "Recharge",
"Notice": "Notice",
"Sign Out": "Sign Out",
"Avatar": "Avatar",
"Account": "Account",
"Balance": "Balance",
"Time": "Time",
"Source": "Source",
"Application Name": "Application Name",
"Total Amount": "Total Amount",
"Change": "Change",
"Password": "Password",
"Replace": "Replace"
"Usage Record": "Usage"
}
}
}

View File

@@ -9,7 +9,9 @@
"Confirm Save App Tip": "保存后将会覆盖高级编排配置,请确保该应用未使用高级编排功能。",
"Connection is invalid": "连接无效",
"Connection type is different": "连接的类型不一致",
"My Apps": "我的应用"
"Input Field Settings": "输入字段编辑",
"My Apps": "我的应用",
"Output Field Settings": "输出字段编辑"
},
"chat": {
"Confirm to clear history": "确认清空该应用的聊天记录?",
@@ -39,28 +41,28 @@
"Tools": "工具"
},
"user": {
"Account": "账号",
"Application Name": "应用名",
"Avatar": "头像",
"Balance": "余额",
"Bill Detail": "账单详情",
"Change": "变更",
"Notice": "通知",
"Old password is error": "旧密码错误",
"OpenAI Account Setting": "OpenAI 账号配置",
"Password": "密码",
"Pay": "充值",
"Personal Information": "个人信息",
"Recharge Record": "充值记录",
"Replace": "更换",
"Set OpenAI Account Failed": "设置 OpenAI 账号异常",
"Sign Out": "登出",
"Source": "来源",
"Time": "时间",
"Total Amount": "总金额",
"Update Password": "修改密码",
"Update password failed": "修改密码异常",
"Update password succseful": "修改密码成功",
"Personal Information": "个人信息",
"Usage Record": "使用记录",
"Recharge Record": "充值记录",
"Notice": "通知",
"Sign Out": "登出",
"Avatar": "头像",
"Account": "账号",
"Balance": "余额",
"Time": "时间",
"Source": "来源",
"Application Name": "应用名",
"Total Amount": "总金额",
"Change": "变更",
"Password": "密码",
"Replace": "更换"
"Usage Record": "使用记录"
}
}
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1690977807626" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9878" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M512 1024a512 512 0 1 1 512-512 512 512 0 0 1-512 512zM512 73.142857a438.857143 438.857143 0 1 0 438.857143 438.857143 438.857143 438.857143 0 0 0-438.857143-438.857143z" p-id="9879"></path><path d="M256 475.428571h512a36.571429 36.571429 0 0 1 36.571429 36.571429 36.571429 36.571429 0 0 1-36.571429 36.571429h-512A36.571429 36.571429 0 0 1 219.428571 512a36.571429 36.571429 0 0 1 36.571429-36.571429z" p-id="9880"></path><path d="M548.571429 256v512a36.571429 36.571429 0 0 1-73.142858 0v-512a36.571429 36.571429 0 0 1 73.142858 0z" p-id="9881"></path></svg>

After

Width:  |  Height:  |  Size: 870 B

View File

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682079057126" class="icon" viewBox="0 0 1322 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2677" xmlns:xlink="http://www.w3.org/1999/xlink" width="36.1484375" height="28"><path d="M952.04654459 837.88839531H336.95615443A113.52706888 113.52706888 0 0 1 223.61160468 724.54384556v-419.79462838h728.43493991a113.52706888 113.52706888 0 0 1 113.34454973 113.34454975V724.54384556a113.52706888 113.52706888 0 0 1-113.34454973 113.34454975zM278.36742569 359.13999928v365.03880736a58.77124787 58.77124787 0 0 0 58.58872873 58.58872874h615.09039016a58.77124787 58.77124787 0 0 0 58.58872874-58.58872874V417.72872802a58.77124787 58.77124787 0 0 0-58.58872874-58.58872874z" p-id="2678"></path><path d="M278.36742569 350.37906772H223.61160468V297.44844068A111.51935577 111.51935577 0 0 1 334.94844068 186.11160469h334.01050924a111.51935577 111.51935577 0 0 1 111.33683598 111.33683599v49.09771996h-54.75582101V297.44844068A56.76353475 56.76353475 0 0 0 668.95894991 240.8674257H334.94844068A56.76353475 56.76353475 0 0 0 278.36742569 297.44844068zM1038.19570329 704.83175018H825.92563707A131.59649008 131.59649008 0 0 1 825.92563707 441.63877003h208.43715913v54.75582103H825.92563707a76.84066906 76.84066906 0 0 0 0 153.86385725h212.27006621z" p-id="2679"></path><path d="M889.80742792 600.43065117h-65.34194654a27.37791082 27.37791082 0 0 1 0-54.75582103h65.34194654a27.37791082 27.37791082 0 0 1-1e-8 54.75582103z" p-id="2680"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -8,7 +8,6 @@ const map = {
copy: require('./icons/copy.svg').default,
chatSend: require('./icons/chatSend.svg').default,
delete: require('./icons/delete.svg').default,
withdraw: require('./icons/withdraw.svg').default,
stop: require('./icons/stop.svg').default,
collectionLight: require('./icons/collectionLight.svg').default,
collectionSolid: require('./icons/collectionSolid.svg').default,
@@ -72,7 +71,8 @@ const map = {
language_en: require('./icons/language/en.svg').default,
language_zh: require('./icons/language/zh.svg').default,
outlink_share: require('./icons/outlink/share.svg').default,
outlink_iframe: require('./icons/outlink/iframe.svg').default
outlink_iframe: require('./icons/outlink/iframe.svg').default,
addCircle: require('./icons/circle/add.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -15,7 +15,7 @@ import {
Input_Template_TFSwitch,
Input_Template_UserChatInput
} from './inputTemplate';
import { ContextExtractEnum } from './flowField';
import { ContextExtractEnum, HttpPropsEnum } from './flowField';
export const ChatModelSystemTip =
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可使用变量,例如 {{language}}';
@@ -448,6 +448,34 @@ export const ContextExtractModule: FlowModuleTemplateType = {
}
]
};
export const HttpModule: FlowModuleTemplateType = {
logo: '/imgs/module/http.png',
name: 'HTTP模块',
intro: '可以发出一个 HTTP POST 请求,实现更为复杂的操作(联网搜索、数据库查询等)',
description: '可以发出一个 HTTP POST 请求,实现更为复杂的操作(联网搜索、数据库查询等)',
flowType: FlowModuleTypeEnum.httpRequest,
inputs: [
{
key: HttpPropsEnum.url,
value: '',
type: FlowInputItemTypeEnum.input,
label: '请求地址',
description: '请求目标地址',
placeholder: 'https://api.fastgpt.run/getInventory',
required: true
},
Input_Template_TFSwitch
],
outputs: [
{
key: HttpPropsEnum.finish,
label: '请求结束',
valueType: FlowValueTypeEnum.boolean,
type: FlowOutputItemTypeEnum.source,
targets: []
}
]
};
export const EmptyModule: FlowModuleTemplateType = {
logo: '/imgs/module/cq.png',
name: '该模块已被移除',
@@ -477,7 +505,7 @@ export const ModuleTemplates = [
},
{
label: 'Agent',
list: [ClassifyQuestionModule, ContextExtractModule]
list: [ClassifyQuestionModule, ContextExtractModule, HttpModule]
}
];
export const ModuleTemplatesFlat = ModuleTemplates.map((templates) => templates.list)?.flat();

View File

@@ -6,3 +6,10 @@ export enum ContextExtractEnum {
failed = 'failed',
fields = 'fields'
}
export enum HttpPropsEnum {
url = 'url',
finish = 'finish',
body = 'body',
response = 'response'
}

View File

@@ -31,7 +31,8 @@ export enum FlowModuleTypeEnum {
tfSwitchNode = 'tfSwitchNode',
answerNode = 'answerNode',
classifyQuestion = 'classifyQuestion',
contentExtract = 'contentExtract'
contentExtract = 'contentExtract',
httpRequest = 'httpRequest'
}
export enum SpecialInputKeyEnum {

View File

@@ -3,9 +3,9 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import { connectToDatabase, App } from '@/service/mongo';
import { AppModuleInputItemType } from '@/types/app';
import { FlowModuleTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { FlowInputItemType } from '@/types/flow';
const chatModelInput = ({
model,
@@ -21,7 +21,7 @@ const chatModelInput = ({
systemPrompt: string;
limitPrompt: string;
kbList: { kbId: string }[];
}): AppModuleInputItemType[] => [
}): FlowInputItemType[] => [
{
key: 'model',
value: model,

View File

@@ -27,7 +27,8 @@ const NodeCQNode = ({
CustomComponent={{
[SpecialInputKeyEnum.agents]: ({
key: agentKey,
value: agents = []
value: agents = [],
...props
}: {
key: string;
value?: ClassifyQuestionAgentItemType[];
@@ -50,7 +51,11 @@ const NodeCQNode = ({
moduleId,
type: 'inputs',
key: agentKey,
value: newInputValue
value: {
...props,
key: agentKey,
value: newInputValue
}
});
onChangeNode({
moduleId,
@@ -77,8 +82,13 @@ const NodeCQNode = ({
);
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: newVal
value: {
...props,
key: agentKey,
value: newVal
}
});
}}
/>
@@ -97,11 +107,16 @@ const NodeCQNode = ({
type: FlowOutputItemTypeEnum.hidden,
targets: []
});
onChangeNode({
moduleId,
type: 'inputs',
key: agentKey,
value: newInputValue
value: {
...props,
key: agentKey,
value: newInputValue
}
});
onChangeNode({
moduleId,

View File

@@ -48,33 +48,32 @@ const NodeChat = ({
onchange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: inputItem.key,
value: e
value: {
...inputItem,
value: e
}
});
// update max tokens
const model = chatModelList.find((item) => item.model === e);
const model =
chatModelList.find((item) => item.model === e) || chatModelList[0];
if (!model) return;
onChangeNode({
moduleId,
type: 'inputs',
key: 'maxToken',
valueKey: 'markList',
value: [
{ label: '100', value: 100 },
{ label: `${model.contextMaxToken}`, value: model.contextMaxToken }
]
});
onChangeNode({
moduleId,
key: 'maxToken',
valueKey: 'max',
value: model.contextMaxToken
});
onChangeNode({
moduleId,
key: 'maxToken',
valueKey: 'value',
value: model.contextMaxToken / 2
value: {
...inputs.find((input) => input.key === 'maxToken'),
markList: [
{ label: '100', value: 100 },
{ label: `${model.contextMaxToken}`, value: model.contextMaxToken }
],
max: model.contextMaxToken,
value: model.contextMaxToken / 2
}
});
}}
/>
@@ -100,8 +99,12 @@ const NodeChat = ({
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: inputItem.key,
value: e
value: {
...inputItem,
value: e
}
});
}}
/>
@@ -115,7 +118,11 @@ const NodeChat = ({
<>
<Divider text="Output" />
<Container>
<RenderOutput flowOutputList={outputs} />
<RenderOutput
onChangeNode={onChangeNode}
moduleId={moduleId}
flowOutputList={outputs}
/>
</Container>
</>
)}

View File

@@ -31,10 +31,9 @@ const NodeExtract = ({
flowInputList={inputs}
CustomComponent={{
[ContextExtractEnum.extractKeys]: ({
key,
value: extractKeys = []
value: extractKeys = [],
...props
}: {
key: string;
value?: ContextExtractAgentItemType[];
}) => (
<Box pt={2}>
@@ -97,7 +96,10 @@ const NodeExtract = ({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: newInputValue
value: {
...props,
value: newInputValue
}
});
onChangeNode({
moduleId,
@@ -121,7 +123,7 @@ const NodeExtract = ({
</Container>
<Divider text="Output" />
<Container>
<RenderOutput flowOutputList={outputs} />
<RenderOutput onChangeNode={onChangeNode} moduleId={moduleId} flowOutputList={outputs} />
</Container>
{!!editExtractFiled && (
@@ -160,7 +162,10 @@ const NodeExtract = ({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: newInputs
value: {
...inputs.find((input) => input.key === ContextExtractEnum.extractKeys),
value: newInputs
}
});
onChangeNode({
moduleId,

View File

@@ -18,7 +18,11 @@ const NodeHistory = ({
</Container>
<Divider text="Output" />
<Container>
<RenderOutput flowOutputList={outputs} />
<RenderOutput
onChangeNode={onChangeNode}
moduleId={props.moduleId}
flowOutputList={outputs}
/>
</Container>
</NodeCard>
);

View File

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

View File

@@ -81,14 +81,19 @@ const NodeKbSearch = ({
onChangeNode={onChangeNode}
flowInputList={inputs}
CustomComponent={{
kbList: ({ key, value }) => (
kbList: ({ key, value, ...props }) => (
<KBSelect
activeKbs={value}
onChange={(e) => {
onChangeNode({
moduleId,
key,
value: e
type: 'inputs',
value: {
...props,
key,
value: e
}
});
}}
/>
@@ -98,7 +103,7 @@ const NodeKbSearch = ({
</Container>
<Divider text="Output" />
<Container>
<RenderOutput flowOutputList={outputs} />
<RenderOutput onChangeNode={onChangeNode} moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);

View File

@@ -9,7 +9,7 @@ import { FlowValueTypeEnum } from '@/constants/flow';
import SourceHandle from '../render/SourceHandle';
const QuestionInputNode = ({
data: { inputs, outputs, onChangeNode, ...props }
data: { inputs, outputs, ...props }
}: NodeProps<FlowModuleItemType>) => {
return (
<NodeCard minW={'240px'} {...props}>

View File

@@ -14,7 +14,7 @@ const NodeUserGuide = ({
data: { inputs, outputs, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const welcomeText = useMemo(
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText)?.value,
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
[inputs]
);
@@ -30,22 +30,27 @@ const NodeUserGuide = ({
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
<Textarea
className="nodrag"
rows={6}
resize={'both'}
defaultValue={welcomeText}
bg={'myWhite.500'}
placeholder={welcomeTextTip}
onChange={(e) => {
onChangeNode({
moduleId: props.moduleId,
key: SystemInputEnum.welcomeText,
type: 'inputs',
value: e.target.value
});
}}
/>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={welcomeTextTip}
onChange={(e) => {
onChangeNode({
moduleId: props.moduleId,
key: SystemInputEnum.welcomeText,
type: 'inputs',
value: {
...welcomeText,
value: e.target.value
}
});
}}
/>
)}
</>
</Container>
</NodeCard>

View File

@@ -40,10 +40,13 @@ const NodeUserGuide = ({
moduleId: props.moduleId,
key: SystemInputEnum.variables,
type: 'inputs',
value
value: {
...inputs.find((item) => item.key === SystemInputEnum.variables),
value
}
});
},
[onChangeNode, props.moduleId]
[inputs, onChangeNode, props.moduleId]
);
const onclickSubmit = useCallback(

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import type { FlowInputItemType, FlowModuleItemType } from '@/types/flow';
import {
Box,
@@ -8,38 +8,129 @@ import {
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper
NumberDecrementStepper,
Flex
} from '@chakra-ui/react';
import { FlowInputItemTypeEnum } from '@/constants/flow';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import dynamic from 'next/dynamic';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
import TargetHandle from './TargetHandle';
import MyIcon from '@/components/Icon';
const SetInputFieldModal = dynamic(() => import('../modules/SetInputFieldModal'));
export 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'}>
*
moduleId,
inputKey,
onChangeNode,
...item
}: FlowInputItemType & {
moduleId: string;
inputKey: string;
onChangeNode: FlowModuleItemType['onChangeNode'];
}) => {
const { required = false, description, edit, label, type, valueType } = item;
const [editField, setEditField] = useState<FlowInputItemType>();
return (
<Flex alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{label}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
{required && (
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
)}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
</Box>
);
{(type === FlowInputItemTypeEnum.target || valueType) && (
<TargetHandle handleKey={inputKey} valueType={valueType} />
)}
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
...item,
key: inputKey
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
{
console.log(moduleId, inputKey, valueType);
}
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField && (
<SetInputFieldModal
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(data) => {
// same key
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'inputs',
key: inputKey,
value: data
});
} else {
// diff key. del and add
onChangeNode({
moduleId,
type: 'delInput',
key: editField.key,
value: ''
});
onChangeNode({
moduleId,
type: 'addInput',
key: editField.key,
value: data
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
const RenderBody = ({
flowInputList,
@@ -59,13 +150,12 @@ const RenderBody = ({
item.type !== FlowInputItemTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
{!!item.label && (
<Label required={item.required} description={item.description}>
{item.label}
{(item.type === FlowInputItemTypeEnum.target || item.valueType) && (
<TargetHandle handleKey={item.key} valueType={item.valueType} />
)}
</Label>
<Label
moduleId={moduleId}
onChangeNode={onChangeNode}
inputKey={item.key}
{...item}
/>
)}
<Box mt={2} className={'nodrag'}>
{item.type === FlowInputItemTypeEnum.numberInput && (
@@ -76,8 +166,12 @@ const RenderBody = ({
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: Number(e)
value: {
...item,
value: Number(e)
}
});
}}
>
@@ -95,8 +189,12 @@ const RenderBody = ({
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: e.target.value
value: {
...item,
value: e.target.value
}
});
}}
/>
@@ -110,8 +208,12 @@ const RenderBody = ({
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: e.target.value
value: {
...item,
value: e.target.value
}
});
}}
/>
@@ -124,8 +226,12 @@ const RenderBody = ({
onchange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: e
value: {
...item,
value: e
}
});
}}
/>
@@ -142,8 +248,12 @@ const RenderBody = ({
onChange={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: item.key,
value: e
value: {
...item,
value: e
}
});
}}
/>

View File

@@ -1,37 +1,147 @@
import React from 'react';
import type { FlowOutputItemType } from '@/types/flow';
import React, { useCallback, useState } from 'react';
import type { FlowModuleItemType, FlowOutputItemType } from '@/types/flow';
import { Box, Flex } from '@chakra-ui/react';
import { FlowOutputItemTypeEnum } from '@/constants/flow';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { Handle, Position } from 'reactflow';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from './SourceHandle';
import MyIcon from '@/components/Icon';
import dynamic from 'next/dynamic';
const SetOutputFieldModal = dynamic(() => import('../modules/SetOutputFieldModal'));
const Label = ({
children,
description
}: {
children: React.ReactNode | string;
description?: string;
}) => (
<Flex as={'label'} justifyContent={'right'} alignItems={'center'} position={'relative'}>
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
{children}
</Flex>
);
moduleId,
outputKey,
delOutputByKey,
updateOutput,
onChangeNode,
addUpdateOutput,
...item
}: FlowOutputItemType & {
outputKey: string;
moduleId: string;
delOutputByKey: (key: string) => void;
updateOutput: (key: string, val: FlowOutputItemType) => void;
addUpdateOutput: (val: FlowOutputItemType) => void;
onChangeNode: FlowModuleItemType['onChangeNode'];
}) => {
const { label, description, edit } = item;
const [editField, setEditField] = useState<FlowOutputItemType>();
return (
<Flex as={'label'} justifyContent={'right'} alignItems={'center'} position={'relative'}>
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
...item,
key: outputKey
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => delOutputByKey(outputKey)}
/>
</>
)}
{description && (
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box>{label}</Box>
{!!editField && (
<SetOutputFieldModal
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(data) => {
console.log(data); // same key
if (editField.key === data.key) {
updateOutput(data.key, data);
} else {
delOutputByKey(editField.key);
addUpdateOutput(data);
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
const RenderOutput = ({
moduleId,
flowOutputList,
onChangeNode
}: {
moduleId: string;
flowOutputList: FlowOutputItemType[];
onChangeNode: FlowModuleItemType['onChangeNode'];
}) => {
const delOutputByKey = useCallback(
(key: string) => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: flowOutputList.filter((output) => output.key !== key)
});
},
[moduleId, flowOutputList, onChangeNode]
);
const updateOutput = useCallback(
(key: string, val: FlowOutputItemType) => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: flowOutputList.map((output) => (output.key === key ? val : output))
});
},
[flowOutputList, moduleId, onChangeNode]
);
const addUpdateOutput = useCallback(
(val: FlowOutputItemType) => {
onChangeNode({
moduleId,
type: 'outputs',
key: '',
value: flowOutputList.concat(val)
});
},
[flowOutputList, moduleId, onChangeNode]
);
const RenderOutput = ({ 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>
<Label
moduleId={moduleId}
onChangeNode={onChangeNode}
outputKey={item.key}
delOutputByKey={delOutputByKey}
addUpdateOutput={addUpdateOutput}
updateOutput={updateOutput}
{...item}
/>
<Box mt={FlowOutputItemTypeEnum.answer ? 0 : 2} className={'nodrag'}>
{item.type === FlowOutputItemTypeEnum.source && (
<SourceHandle handleKey={item.key} valueType={item.valueType} />

View File

@@ -16,7 +16,7 @@ const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[FlowValueTypeEnum.any] as any),
[]
[valueType]
);
return (

View File

@@ -71,6 +71,9 @@ const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'),
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'), {
ssr: false
});
const NodeHttp = dynamic(() => import('./components/Nodes/NodeHttp'), {
ssr: false
});
import 'reactflow/dist/style.css';
import styles from './index.module.scss';
@@ -87,7 +90,8 @@ const nodeTypes = {
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
[FlowModuleTypeEnum.contentExtract]: NodeExtract
[FlowModuleTypeEnum.contentExtract]: NodeExtract,
[FlowModuleTypeEnum.httpRequest]: NodeHttp
// [FlowModuleTypeEnum.empty]: EmptyModule
};
const edgeTypes = {
@@ -124,11 +128,10 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
const flow2AppModules = useCallback(() => {
const modules: AppModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
position: item.position,
flowType: item.data.flowType,
position: item.position,
inputs: item.data.inputs.map((item) => ({
key: item.key,
value: item.value,
...item,
connected: item.type !== FlowInputItemTypeEnum.target
})),
outputs: item.data.outputs.map((item) => ({
@@ -163,7 +166,7 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
return modules;
}, [edges, nodes]);
const onChangeNode = useCallback(
({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
({ moduleId, key, type = 'inputs', value }: FlowModuleItemChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
@@ -172,18 +175,43 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
...node,
data: {
...node.data,
inputs: node.data.inputs.map((item) => {
if (item.key === key) {
return {
...item,
[valueKey]: value
};
}
return item;
})
inputs: node.data.inputs.map((item) => (item.key === key ? value : item))
}
};
}
if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
return {
...node,
data: {
...node.data,
inputs: node.data.inputs
}
};
}
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.concat(value)
}
};
}
if (type === 'delInput') {
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.filter((item) => item.key !== key)
}
};
}
console.log(value);
return {
...node,

View File

@@ -59,7 +59,7 @@ const FileSelect = ({
)}
{isCsv && (
<Box
my={3}
mt={1}
cursor={'pointer'}
textDecoration={'underline'}
color={'myBlue.600'}

View File

@@ -0,0 +1,31 @@
import { sseResponseEventEnum, TaskResponseKeyEnum } from '@/constants/chat';
import { sseResponse } from '@/service/utils/tools';
import { textAdaptGptResponse } from '@/utils/adapt';
import type { NextApiResponse } from 'next';
export type AnswerProps = {
res: NextApiResponse;
text: string;
stream: boolean;
};
export type AnswerResponse = {
[TaskResponseKeyEnum.answerText]: string;
};
export const dispatchAnswer = (props: Record<string, any>): AnswerResponse => {
const { res, text = '', stream } = props as AnswerProps;
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: text.replace(/\\n/g, '\n')
})
});
}
return {
[TaskResponseKeyEnum.answerText]: text
};
};

View File

@@ -51,6 +51,11 @@ export type ContextExtractAgentItemType = {
key: string;
required: boolean;
};
export type HttpFieldItemType = {
label: string;
key: string;
type: `${FlowValueTypeEnum}`;
};
export type VariableItemType = {
id: string;
@@ -63,12 +68,11 @@ export type VariableItemType = {
};
/* app module */
export type AppModuleInputItemType = { key: string; value?: any; connected?: boolean };
export type AppModuleItemType = {
moduleId: string;
position?: XYPosition;
flowType: `${FlowModuleTypeEnum}`;
inputs: AppModuleInputItemType[];
inputs: FlowInputItemType[];
outputs: FlowOutputItemType[];
};

View File

@@ -10,10 +10,9 @@ import { FlowModuleTypeEnum } from '@/constants/flow';
export type FlowModuleItemChangeProps = {
moduleId: string;
type?: 'inputs' | 'outputs';
type: 'inputs' | 'outputs' | 'addInput' | 'delInput';
key: string;
value: any;
valueKey?: keyof FlowInputItemType & keyof FlowBodyItemType;
};
export type FlowInputItemType = {
@@ -22,6 +21,7 @@ export type FlowInputItemType = {
valueType?: `${FlowValueTypeEnum}`;
type: `${FlowInputItemTypeEnum}`;
label: string;
edit?: boolean;
connected?: boolean;
description?: string;
placeholder?: string;
@@ -40,6 +40,7 @@ export type FlowOutputTargetItemType = {
export type FlowOutputItemType = {
key: string; // 字段名
label?: string;
edit?: boolean;
description?: string;
valueType?: `${FlowValueTypeEnum}`;
type?: `${FlowOutputItemTypeEnum}`;

View File

@@ -75,11 +75,17 @@ export const appModule2FlowNode = ({
const template =
ModuleTemplatesFlat.find((template) => template.flowType === item.flowType) || EmptyModule;
const concatInputs = template.inputs.concat(
item.inputs.filter(
(input) => input.label && !template.inputs.find((item) => item.key === input.key)
)
);
// replace item data
const moduleItem: FlowModuleItemType = {
...item,
...template,
inputs: template.inputs.map((templateInput) => {
inputs: concatInputs.map((templateInput) => {
// use latest inputs
const itemInput = item.inputs.find((item) => item.key === templateInput.key) || templateInput;
return {
@@ -87,7 +93,6 @@ export const appModule2FlowNode = ({
value: itemInput.value
};
}),
// 合并 template 和数据库,文案以 template 为准
outputs: item.outputs.map((output) => {
// unChange outputs
const templateOutput = template.outputs.find((item) => item.key === output.key);

View File

@@ -1,9 +1,10 @@
import type { AppModuleInputItemType, AppModuleItemType, VariableItemType } from '@/types/app';
import type { AppModuleItemType, VariableItemType } from '@/types/app';
import { chatModelList, vectorModelList } from '@/store/static';
import { FlowModuleTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
import { SystemInputEnum } from '@/constants/app';
import { TaskResponseKeyEnum } from '@/constants/chat';
import type { SelectedKbType } from '@/types/plugin';
import { FlowInputItemType } from '@/types/flow';
export type EditFormType = {
chatModel: {
@@ -64,7 +65,7 @@ export const appModules2Form = (modules: AppModuleItemType[]) => {
key
}: {
formKey: string;
inputs: AppModuleInputItemType[];
inputs: FlowInputItemType[];
key: string;
}) => {
const propertyPath = formKey.split('.');
@@ -148,7 +149,7 @@ export const appModules2Form = (modules: AppModuleItemType[]) => {
return defaultAppForm;
};
const chatModelInput = (formData: EditFormType): AppModuleInputItemType[] => [
const chatModelInput = (formData: EditFormType): FlowInputItemType[] => [
{
key: 'model',
value: formData.chatModel.model,