V4.6.5-alpha (#609)

This commit is contained in:
Archer
2023-12-15 15:57:39 +08:00
committed by GitHub
parent dd7b4b98ae
commit 05bf1b2265
127 changed files with 4283 additions and 2315 deletions

View File

@@ -21,6 +21,7 @@ function Row({
value?: string | number;
rawDom?: React.ReactNode;
}) {
const { t } = useTranslation();
const theme = useTheme();
const val = value || rawDom;
const strValue = `${value}`;
@@ -29,7 +30,7 @@ function Row({
return val !== undefined && val !== '' && val !== 'undefined' ? (
<Box mb={3}>
<Box fontSize={['sm', 'md']} mb={isCodeBlock ? 0 : 1} flex={'0 0 90px'}>
{label}:
{t(label)}:
</Box>
<Box
borderRadius={'md'}
@@ -69,12 +70,12 @@ const WholeResponseModal = ({
alt={''}
w={['14px', '16px']}
/>
{item.moduleName}
{t(item.moduleName)}
</Flex>
),
id: `${i}`
})),
[response]
[response, t]
);
const [currentTab, setCurrentTab] = useState(`0`);
@@ -103,26 +104,33 @@ const WholeResponseModal = ({
<Tabs list={list} activeId={currentTab} onChange={setCurrentTab} />
</Box>
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<Row label={t('chat.response.module name')} value={activeModule?.moduleName} />
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
{activeModule?.price !== undefined && (
<Row
label={t('chat.response.module price')}
label={t('core.chat.response.module price')}
value={`${formatPrice(activeModule?.price)}`}
/>
)}
<Row
label={t('chat.response.module time')}
label={t('core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('chat.response.module tokens')} value={`${activeModule?.tokens}`} />
<Row label={t('chat.response.module model')} value={activeModule?.model} />
<Row label={t('chat.response.module query')} value={activeModule?.query} />
<Row label={t('core.chat.response.module tokens')} value={`${activeModule?.tokens}`} />
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
<Row
label={t('core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
{/* ai chat */}
<Row label={t('chat.response.module temperature')} value={activeModule?.temperature} />
<Row label={t('chat.response.module maxToken')} value={activeModule?.maxToken} />
<Row
label={t('chat.response.module historyPreview')}
label={t('core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row label={t('core.chat.response.module maxToken')} value={activeModule?.maxToken} />
<Row
label={t('core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<>
@@ -148,7 +156,7 @@ const WholeResponseModal = ({
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('chat.response.module quoteList')}
label={t('core.chat.response.module quoteList')}
value={`~~~json\n${JSON.stringify(activeModule.quoteList, null, 2)}`}
/>
)}
@@ -161,27 +169,27 @@ const WholeResponseModal = ({
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row label={t('chat.response.module similarity')} value={activeModule?.similarity} />
<Row label={t('chat.response.module limit')} value={activeModule?.limit} />
<Row label={t('core.chat.response.module similarity')} value={activeModule?.similarity} />
<Row label={t('core.chat.response.module limit')} value={activeModule?.limit} />
{/* classify question */}
<Row
label={t('chat.response.module cq')}
label={t('core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
<Row label={t('chat.response.module cq result')} value={activeModule?.cqResult} />
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
{/* extract */}
<Row
label={t('chat.response.module extract description')}
label={t('core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
{activeModule?.extractResult && (
<Row
label={t('chat.response.module extract result')}
label={t('core.chat.response.module extract result')}
value={`~~~json\n${JSON.stringify(activeModule?.extractResult, null, 2)}`}
/>
)}
@@ -189,13 +197,13 @@ const WholeResponseModal = ({
{/* http */}
{activeModule?.body && (
<Row
label={t('chat.response.module http body')}
label={t('core.chat.response.module http body')}
value={`~~~json\n${JSON.stringify(activeModule?.body, null, 2)}`}
/>
)}
{activeModule?.httpResult && (
<Row
label={t('chat.response.module http result')}
label={t('core.chat.response.module http result')}
value={`~~~json\n${JSON.stringify(activeModule?.httpResult, null, 2)}`}
/>
)}
@@ -203,10 +211,13 @@ const WholeResponseModal = ({
{/* plugin */}
{activeModule?.pluginOutput && (
<Row
label={t('chat.response.plugin output')}
label={t('core.chat.response.plugin output')}
value={`~~~json\n${JSON.stringify(activeModule?.pluginOutput, null, 2)}`}
/>
)}
{/* text editor */}
<Row label={t('core.chat.response.text output')} value={activeModule?.textOutput} />
</Box>
</Flex>
</MyModal>

View File

@@ -500,7 +500,7 @@ const ChatBox = (
return {
bg: colorMap[chatContent.status] || colorMap.loading,
name: chatContent.moduleName || t('common.Loading')
name: t(chatContent.moduleName || '') || t('common.Loading')
};
}, [chatHistory, isChatting, t]);
/* style end */
@@ -517,7 +517,7 @@ const ChatBox = (
};
}, [router.query]);
// add guide text listener
// add listener
useEffect(() => {
const windowMessage = ({ data }: MessageEvent<{ type: 'sendPrompt'; text: string }>) => {
if (data?.type === 'sendPrompt' && data?.text) {
@@ -536,9 +536,9 @@ const ChatBox = (
});
return () => {
window.removeEventListener('message', windowMessage);
eventBus.off(EventNameEnum.sendQuestion);
eventBus.off(EventNameEnum.editQuestion);
window.removeEventListener('message', windowMessage);
};
}, [handleSubmit, resetInputVal, sendPrompt]);

View File

@@ -0,0 +1,113 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalBody,
ModalFooter,
Textarea,
TextareaProps,
useDisclosure
} from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import MyIcon from '@/components/Icon';
import MyModal from '@/components/MyModal';
type Props = TextareaProps & {
title?: string;
showSetModalModeIcon?: boolean;
// variables: string[];
};
const PromptTextarea = (props: Props) => {
const { t } = useTranslation();
const { title = t('core.app.edit.Prompt Editor'), value, ...childProps } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Editor {...childProps} value={value} showSetModalModeIcon onSetModalMode={onOpen} />
{isOpen && (
<MyModal iconSrc="/imgs/modal/edit.svg" title={title} isOpen onClose={onClose}>
<ModalBody>
<Editor
{...childProps}
value={value}
minH={'300px'}
maxH={'auto'}
minW={['100%', '512px']}
showSetModalModeIcon={false}
/>
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
)}
</>
);
};
export default PromptTextarea;
const Editor = React.memo(function Editor({
showSetModalModeIcon = true,
onSetModalMode,
...props
}: Props & { onSetModalMode?: () => void }) {
const { t } = useTranslation();
return (
<Box h={'100%'} w={'100%'} position={'relative'}>
<Textarea wordBreak={'break-all'} maxW={'100%'} {...props} />
{showSetModalModeIcon && (
<Box
zIndex={1}
position={'absolute'}
bottom={1}
right={2}
cursor={'pointer'}
onClick={onSetModalMode}
>
<MyTooltip label={t('common.ui.textarea.Magnifying')}>
<MyIcon name={'fullScreenLight'} w={'14px'} color={'myGray.600'} />
</MyTooltip>
</Box>
)}
</Box>
);
});
const VariableSelectBlock = React.memo(function VariableSelectBlock({
variables
}: {
variables: string[];
}) {
return <></>;
});
const Placeholder = React.memo(function Placeholder({
placeholder = ''
}: {
placeholder?: string;
}) {
return (
<Box
zIndex={0}
userSelect={'none'}
color={'myGray.400'}
px={3}
py={2}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
fontSize={'sm'}
>
{placeholder}
</Box>
);
});

View File

@@ -26,6 +26,7 @@ import type { AIChatModuleProps } from '@fastgpt/global/core/module/node/type.d'
import type { AppSimpleEditConfigTemplateType } from '@fastgpt/global/core/app/type.d';
import { SimpleModeTemplate_FastGPT_Universal } from '@/global/core/app/constants';
import { getDocPath } from '@/web/common/system/doc';
import PromptTextarea from '@/components/common/Textarea/PromptTextarea';
const PromptTemplate = dynamic(() => import('@/components/PromptTemplate'));
@@ -187,14 +188,18 @@ const AIChatSettingsModal = ({
</Box>
</Flex>
<Textarea
rows={6}
placeholder={
t('template.Quote Content Tip', { default: Prompt_QuoteTemplateList[0].value }) ||
''
}
borderColor={'myGray.100'}
{...register(ModuleInputKeyEnum.aiChatQuoteTemplate)}
<PromptTextarea
bg={'myWhite.400'}
rows={8}
placeholder={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
showSetModalModeIcon
value={getValues(ModuleInputKeyEnum.aiChatQuoteTemplate)}
onChange={(e) => {
setValue(ModuleInputKeyEnum.aiChatQuoteTemplate, e.target.value);
setRefresh(!refresh);
}}
/>
</Box>
)}
@@ -209,13 +214,18 @@ const AIChatSettingsModal = ({
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
<Textarea
<PromptTextarea
bg={'myWhite.400'}
rows={11}
placeholder={
t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value }) || ''
}
borderColor={'myGray.100'}
{...register(ModuleInputKeyEnum.aiChatQuotePrompt)}
placeholder={t('template.Quote Prompt Tip', {
default: Prompt_QuotePromptList[0].value
})}
showSetModalModeIcon
value={getValues(ModuleInputKeyEnum.aiChatQuotePrompt)}
onChange={(e) => {
setValue(ModuleInputKeyEnum.aiChatQuotePrompt, e.target.value);
setRefresh(!refresh);
}}
/>
</Box>
)}

View File

@@ -28,8 +28,8 @@ import React, {
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@/web/common/hooks/useToast';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
@@ -174,7 +174,7 @@ export const FlowProvider = ({
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion) {
return ModuleDataTypeEnum.boolean;
return ModuleIOValueTypeEnum.string;
}
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
@@ -193,8 +193,8 @@ export const FlowProvider = ({
});
}
if (
sourceType !== ModuleDataTypeEnum.any &&
targetType !== ModuleDataTypeEnum.any &&
sourceType !== ModuleIOValueTypeEnum.any &&
targetType !== ModuleIOValueTypeEnum.any &&
sourceType !== targetType
) {
return toast({
@@ -207,8 +207,7 @@ export const FlowProvider = ({
addEdge(
{
...connect,
type: 'buttonedge',
animated: true,
type: EDGE_TYPE,
data: {
onDelete: onDelConnect
}
@@ -228,6 +227,7 @@ export const FlowProvider = ({
[setEdges, setNodes]
);
/* change */
const onChangeNode = useCallback(
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
setNodes((nodes) =>
@@ -434,51 +434,3 @@ export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowNodeChangeProps) => {
eventBus.emit(EventNameEnum.updaterNode, e);
};
export function flowNode2Modules({
nodes,
edges
}: {
nodes: Node<FlowModuleItemType, string | undefined>[];
edges: Edge<any>[];
}) {
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
avatar: item.data.avatar,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((input) => ({
...input,
connected: false
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowNodeOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected = !!edges.find(
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter(
(edge) =>
edge.source === module.moduleId && edge.sourceHandle === output.key && edge.targetHandle
)
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
}

View File

@@ -3,13 +3,25 @@ import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowProviderStore } from './FlowProvider';
import { useFlowProviderStore, type useFlowProviderStoreType } from './FlowProvider';
const ImportSettings = ({ onClose }: { onClose: () => void }) => {
type Props = {
onClose: () => void;
};
const ImportSettings = ({
onClose,
setNodes,
setEdges,
initData
}: Props & {
setNodes: useFlowProviderStoreType['setNodes'];
setEdges: useFlowProviderStoreType['setEdges'];
initData: useFlowProviderStoreType['initData'];
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [value, setValue] = useState('');
const { setNodes, setEdges, initData } = useFlowProviderStore();
return (
<MyModal
@@ -56,4 +68,8 @@ const ImportSettings = ({ onClose }: { onClose: () => void }) => {
);
};
export default React.memo(ImportSettings);
export default React.memo(function (props: Props) {
const { setNodes, setEdges, initData } = useFlowProviderStore();
return <ImportSettings {...props} setNodes={setNodes} setEdges={setEdges} initData={initData} />;
});

View File

@@ -7,13 +7,12 @@ import type {
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
import { useFlowProviderStore } from './FlowProvider';
import { useFlowProviderStore, type useFlowProviderStoreType } from './FlowProvider';
import { customAlphabet } from 'nanoid';
import { appModule2FlowNode } from '@/utils/adapt';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyIcon from '@/components/Icon';
import EmptyTip from '@/components/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
@@ -22,47 +21,32 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { moduleTemplatesList } from '@/web/core/modules/template/system';
import { ModuleTemplateTypeEnum } from '@fastgpt/global/core/module/constants';
enum TemplateTypeEnum {
system = 'system',
plugin = 'plugin'
}
export type ModuleTemplateProps = {
systemTemplates: FlowModuleTemplateType[];
pluginTemplates: FlowModuleTemplateType[];
templates: FlowModuleTemplateType[];
};
type ModuleTemplateListProps = ModuleTemplateProps & {
isOpen: boolean;
onClose: () => void;
};
type RenderListProps = {
templates: FlowModuleTemplateType[];
onClose: () => void;
setNodes: useFlowProviderStoreType['setNodes'];
reactFlowWrapper: useFlowProviderStoreType['reactFlowWrapper'];
};
const ModuleTemplateList = ({
systemTemplates,
pluginTemplates,
templates,
isOpen,
onClose
}: ModuleTemplateProps & {
isOpen: boolean;
onClose: () => void;
onClose,
setNodes,
reactFlowWrapper
}: ModuleTemplateListProps & {
setNodes: useFlowProviderStoreType['setNodes'];
reactFlowWrapper: useFlowProviderStoreType['reactFlowWrapper'];
}) => {
const { t } = useTranslation();
const [templateType, setTemplateType] = React.useState(TemplateTypeEnum.system);
const typeList = useMemo(
() => [
{
type: TemplateTypeEnum.system,
label: t('app.module.System Module'),
child: <RenderList templates={systemTemplates} onClose={onClose} />
},
{
type: TemplateTypeEnum.plugin,
label: t('plugin.Plugin Module'),
child: <RenderList templates={pluginTemplates} onClose={onClose} isPlugin />
}
],
[pluginTemplates, onClose, systemTemplates, t]
);
const TemplateItem = useMemo(
() => typeList.find((item) => item.type === templateType)?.child,
[templateType, typeList]
);
return (
<>
@@ -82,6 +66,7 @@ const ModuleTemplateList = ({
position={'absolute'}
top={'65px'}
left={0}
pt={2}
pb={4}
h={isOpen ? 'calc(100% - 100px)' : '0'}
w={isOpen ? ['100%', '360px'] : '0'}
@@ -92,47 +77,32 @@ const ModuleTemplateList = ({
transition={'.2s ease'}
userSelect={'none'}
>
<Flex pt={4} pb={1} px={5} gap={4} alignItems={'center'} fontSize={['md', 'xl']}>
{typeList.map((item) => (
<Box
key={item.label}
borderBottom={'2px solid transparent'}
{...(item.type === templateType
? {
color: 'myBlue.700',
borderBottomColor: 'myBlue.700',
fontWeight: 'bold'
}
: {
cursor: 'pointer',
onClick: () => setTemplateType(item.type)
})}
>
{item.label}
</Box>
))}
</Flex>
{TemplateItem}
<RenderList
templates={templates}
onClose={onClose}
setNodes={setNodes}
reactFlowWrapper={reactFlowWrapper}
/>
</Flex>
</>
);
};
export default React.memo(ModuleTemplateList);
export default React.memo(function (props: ModuleTemplateListProps) {
const { setNodes, reactFlowWrapper } = useFlowProviderStore();
return <ModuleTemplateList {...props} setNodes={setNodes} reactFlowWrapper={reactFlowWrapper} />;
});
const RenderList = React.memo(function RenderList({
templates,
isPlugin = false,
onClose
}: {
templates: FlowModuleTemplateType[];
isPlugin?: boolean;
onClose: () => void;
}) {
onClose,
setNodes,
reactFlowWrapper
}: RenderListProps) {
const { t } = useTranslation();
const router = useRouter();
const { isPc } = useSystemStore();
const { setNodes, reactFlowWrapper } = useFlowProviderStore();
const { x, y, zoom } = useViewport();
const { setLoading } = useSystemStore();
const { toast } = useToast();
@@ -199,9 +169,9 @@ const RenderList = React.memo(function RenderList({
<Box key={item.type}>
<Flex>
<Box fontWeight={'bold'} flex={1}>
{item.label}
{t(item.label)}
</Box>
{isPlugin && item.type === ModuleTemplateTypeEnum.personalPlugin && (
{/* {isPlugin && item.type === ModuleTemplateTypeEnum.personalPlugin && (
<Flex
alignItems={'center'}
_hover={{ textDecoration: 'underline' }}
@@ -213,7 +183,7 @@ const RenderList = React.memo(function RenderList({
</Box>
<MyIcon name={'common/rightArrowLight'} w={'12px'} />
</Flex>
)}
)} */}
</Flex>
<>
{item.list.map((template) => (
@@ -248,9 +218,9 @@ const RenderList = React.memo(function RenderList({
borderRadius={'0'}
/>
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{template.name}</Box>
<Box color={'black'}>{t(template.name)}</Box>
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'sm'}>
{template.intro}
{t(template.intro)}
</Box>
</Box>
</Flex>

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { useStoreApi, type ConnectionLineComponentProps } from 'reactflow';
const CustomConnection = ({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) => {
const store = useStoreApi();
const { connectionHandleId } = store.getState();
console.log(fromX, fromY, toX, toY, connectionHandleId);
return (
<g>
<path
fill="none"
stroke={connectionHandleId || ''}
strokeWidth={1.5}
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle
cx={toX}
cy={toY}
fill="#fff"
r={3}
stroke={connectionHandleId || ''}
strokeWidth={1.5}
/>
</g>
);
};
export default CustomConnection;

View File

@@ -1,23 +1,33 @@
import React from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from 'reactflow';
import {
SmoothStepEdge,
EdgeLabelRenderer,
EdgeProps,
getSmoothStepPath,
MarkerType
} from 'reactflow';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
const ButtonEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data
}: EdgeProps<{
onDelete: (id: string) => void;
}>) => {
const [edgePath, labelX, labelY] = getBezierPath({
const ButtonEdge = (
props: EdgeProps<{
onDelete: (id: string) => void;
}>
) => {
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
selected,
style = {}
} = props;
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
@@ -26,9 +36,19 @@ const ButtonEdge = ({
targetPosition
});
const edgeStyle = {
...style,
...(selected
? {
strokeWidth: 4,
stroke: '#3370ff'
}
: { strokeWidth: 2, stroke: '#BDC1C5' })
};
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<SmoothStepEdge {...props} style={edgeStyle} />
<EdgeLabelRenderer>
<Flex
alignItems={'center'}
@@ -48,7 +68,11 @@ const ButtonEdge = ({
}}
onClick={() => data?.onDelete(id)}
>
<MyIcon name="closeSolid" w={'100%'} color={'myGray.600'}></MyIcon>
<MyIcon
name="closeSolid"
w={'100%'}
color={selected ? 'myBlue.800' : 'myGray.500'}
></MyIcon>
</Flex>
</EdgeLabelRenderer>
</>

View File

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

View File

@@ -1,195 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@/components/MyModal';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import MySelect from '@/components/Select';
import { FlowValueTypeMap } from '@/web/core/modules/constants/dataType';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
export type EditFieldModeType = 'input' | 'output' | 'pluginInput';
export type EditFieldType = {
type?: `${FlowNodeInputTypeEnum}`; // input type
key: string;
label?: string;
valueType?: `${ModuleDataTypeEnum}`;
description?: string;
required?: boolean;
createSign?: boolean;
};
const FieldEditModal = ({
mode,
defaultField,
onClose,
onSubmit
}: {
mode: EditFieldModeType;
defaultField: EditFieldType;
onClose: () => void;
onSubmit: (data: EditFieldType) => void;
}) => {
const { t } = useTranslation();
const inputTypeList = [
{
label: t('core.module.inputType.target'),
value: FlowNodeInputTypeEnum.target,
valueType: ModuleDataTypeEnum.string
},
{
label: t('core.module.inputType.input'),
value: FlowNodeInputTypeEnum.input,
valueType: ModuleDataTypeEnum.string
},
{
label: t('core.module.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
valueType: ModuleDataTypeEnum.string
},
{
label: t('core.module.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
valueType: ModuleDataTypeEnum.boolean
},
{
label: t('core.module.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
valueType: ModuleDataTypeEnum.selectDataset
}
];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -2)
.map((item) => ({
label: t(item.label),
value: item.value
}));
const { register, getValues, setValue, handleSubmit } = useForm<EditFieldType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
const title = ['input', 'pluginInput'].includes(mode)
? t('app.Input Field Settings')
: t('app.Output Field Settings');
const showValueTypeSelect = useMemo(() => {
return getValues('type') === FlowNodeInputTypeEnum.target || mode === 'output';
}, [getValues, mode, refresh]);
return (
<MyModal isOpen={true} iconSrc="/imgs/module/extract.png" title={title} onClose={onClose}>
<ModalBody minH={'260px'} overflow={'visible'}>
{/* input type select: target, input, textarea.... */}
{mode === 'pluginInput' && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('core.module.Input Type')}</Box>
<MySelect
w={'288px'}
list={inputTypeList}
value={getValues('type')}
onchange={(e: string) => {
const type = e as `${FlowNodeInputTypeEnum}`;
const selectedItem = inputTypeList.find((item) => item.value === type);
setValue('type', type);
setValue('valueType', selectedItem?.valueType);
if (type === FlowNodeInputTypeEnum.selectDataset) {
setValue('label', selectedItem?.label);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{['input', 'pluginInput'].includes(mode) && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('common.Require Input')}</Box>
<Switch {...register('required')} />
</Flex>
)}
{showValueTypeSelect && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<MySelect
w={'288px'}
list={dataTypeSelectList}
value={getValues('valueType')}
onchange={(e: string) => {
const type = e as `${ModuleDataTypeEnum}`;
setValue('valueType', type);
if (
type === ModuleDataTypeEnum.chatHistory ||
type === ModuleDataTypeEnum.datasetQuote
) {
const label = dataTypeSelectList.find((item) => item.value === type)?.label;
setValue('label', label);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Name')}</Box>
<Input
placeholder="预约字段/sql语句……"
{...register('label', { required: '字段名不能为空' })}
/>
</Flex>
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box>
<Input
placeholder="appointment/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
<Flex mb={5} alignItems={'flex-start'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Description')}</Box>
<Textarea placeholder="可选" rows={3} {...register('description')} />
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);
export const defaultInputField: EditFieldType = {
label: '',
key: '',
description: '',
type: FlowNodeInputTypeEnum.target,
valueType: ModuleDataTypeEnum.string,
required: true,
createSign: true
};
export const defaultOutputField: EditFieldType = {
label: '',
key: '',
description: '',
valueType: ModuleDataTypeEnum.string,
required: true,
createSign: true
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
@@ -11,7 +11,7 @@ import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
import MyIcon from '@/components/Icon';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleDataTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import SourceHandle from '../render/SourceHandle';
import MyTooltip from '@/components/MyTooltip';
@@ -19,7 +19,7 @@ import { onChangeNode } from '../../FlowProvider';
const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { moduleId, inputs } = data;
return (
<NodeCard minW={'400px'} {...data}>
@@ -29,28 +29,57 @@ const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
moduleId={moduleId}
flowInputList={inputs}
CustomComponent={{
[ModuleInputKeyEnum.agents]: ({
key: agentKey,
value: agents = [],
...props
}: {
key: string;
value?: ClassifyQuestionAgentItemType[];
}) => (
<Box>
{agents.map((item, i) => (
<Box key={item.key} mb={4}>
<Flex alignItems={'center'}>
<MyTooltip label={t('common.Delete')}>
<MyIcon
[ModuleInputKeyEnum.agents]: ({ key: agentKey, value = [], ...props }) => {
const agents = value as ClassifyQuestionAgentItemType[];
return (
<Box>
{agents.map((item, i) => (
<Box key={item.key} mb={4}>
<Flex alignItems={'center'}>
<MyTooltip label={t('common.Delete')}>
<MyIcon
mt={1}
mr={2}
name={'minus'}
w={'14px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.filter((input) => input.key !== item.key)
}
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</MyTooltip>
<Box flex={1}>{i + 1}</Box>
</Flex>
<Box position={'relative'}>
<Textarea
rows={2}
mt={1}
mr={2}
name={'minus'}
w={'14px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
defaultValue={item.value}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
moduleId,
type: 'updateInput',
@@ -58,80 +87,50 @@ const NodeCQNode = ({ data }: NodeProps<FlowModuleItemType>) => {
value: {
...props,
key: agentKey,
value: agents.filter((input) => input.key !== item.key)
value: newVal
}
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</MyTooltip>
<Box flex={1}>{i + 1}</Box>
</Flex>
<Box position={'relative'}>
<Textarea
rows={2}
mt={1}
defaultValue={item.value}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: newVal
}
});
}}
/>
<SourceHandle handleKey={item.key} valueType={ModuleDataTypeEnum.string} />
<SourceHandle
handleKey={item.key}
valueType={ModuleIOValueTypeEnum.string}
/>
</Box>
</Box>
</Box>
))}
<Button
onClick={() => {
const key = nanoid();
))}
<Button
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: {
...props,
onChangeNode({
moduleId,
type: 'updateInput',
key: agentKey,
value: agents.concat({ value: '', key })
}
});
value: {
...props,
key: agentKey,
value: agents.concat({ value: '', key })
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key,
label: '',
type: FlowNodeOutputTypeEnum.hidden,
targets: []
}
});
}}
>
</Button>
</Box>
)
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key,
label: '',
type: FlowNodeOutputTypeEnum.hidden,
targets: []
}
});
}}
>
</Button>
</Box>
);
}
}}
/>
</Container>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
const NodeAnswer = ({ data }: NodeProps<FlowModuleItemType>) => {

View File

@@ -0,0 +1,85 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/module/type';
import { useForm } from 'react-hook-form';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
export const defaultField = {
desc: '',
key: '',
required: true,
enum: ''
};
const ExtractFieldModal = ({
defaultField,
onClose,
onSubmit
}: {
defaultField: ContextExtractAgentItemType;
onClose: () => void;
onSubmit: (data: ContextExtractAgentItemType) => void;
}) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField
});
return (
<MyModal
isOpen={true}
iconSrc="/imgs/module/extract.png"
title={t('core.module.extract.Field Setting Title')}
onClose={onClose}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('common.Require Input')}</Box>
<Switch {...register('required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box>
<Input placeholder="name/age/sql" {...register('key', { required: true })} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Description')}</Box>
<Input
placeholder={t('core.module.extract.Field Description Placeholder')}
{...register('desc', { required: true })}
/>
</Flex>
<Box mt={5}>
<Flex alignItems={'center'}>
{t('core.module.extract.Enum Value')}({t('common.choosable')})
<MyTooltip label={t('core.module.extract.Enum Description')} forceShow>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Textarea rows={5} placeholder={'apple\npeach\nwatermelon'} {...register('enum')} />
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ExtractFieldModal);

View File

@@ -3,25 +3,24 @@ import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@c
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../modules/NodeCard';
import Container from '../modules/Container';
import NodeCard from '../../render/NodeCard';
import Container from '../../modules/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import Divider from '../modules/Divider';
import RenderInput from '../../render/RenderInput';
import Divider from '../../modules/Divider';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/module/type';
import RenderOutput from '../render/RenderOutput';
import RenderOutput from '../../render/RenderOutput';
import MyIcon from '@/components/Icon';
import ExtractFieldModal from '../modules/ExtractFieldModal';
import ExtractFieldModal, { defaultField } from './ExtractFieldModal';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { useFlowProviderStore, onChangeNode } from '../../FlowProvider';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode } from '../../../FlowProvider';
const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId } = data;
const { t } = useTranslation();
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const { onDelEdge } = useFlowProviderStore();
return (
<NodeCard minW={'400px'} {...data}>
@@ -42,13 +41,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
<Button
variant={'base'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() =>
setEditExtractField({
desc: '',
key: '',
required: true
})
}
onClick={() => setEditExtractField(defaultField)}
>
</Button>
@@ -150,7 +143,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
key: data.key,
label: `提取结果-${data.desc}`,
description: '无法提取时不会返回',
valueType: ModuleDataTypeEnum.string,
valueType: ModuleIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
targets: []
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
@@ -13,10 +13,9 @@ import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import { onChangeNode } from '../../FlowProvider';
const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
@@ -25,54 +24,10 @@ const NodeHttp = ({ data }: NodeProps<FlowModuleItemType>) => {
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
<Button
variant={'base'}
mt={5}
leftIcon={<SmallAddIcon />}
onClick={() => {
const key = nanoid();
onChangeNode({
moduleId,
type: 'addInput',
key,
value: {
key,
valueType: ModuleDataTypeEnum.string,
type: FlowNodeInputTypeEnum.target,
label: `入参${inputs.length - 1}`,
edit: true
}
});
}}
>
</Button>
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
<Box textAlign={'right'} mt={5}>
<Button
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: nanoid(),
label: `出参${outputs.length}`,
valueType: ModuleDataTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
}}
>
</Button>
</Box>
</Container>
</NodeCard>
);

View File

@@ -1,25 +1,51 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { onChangeNode } from '../../FlowProvider';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import Container from '../modules/Container';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from '../render/SourceHandle';
import { defaultInputField, type EditFieldType } from '../modules/FieldEditModal';
import { useToast } from '@/web/common/hooks/useToast';
import type {
EditNodeFieldType,
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/module/node/type.d';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
const FieldEditModal = dynamic(() => import('../render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
};
const createEditField = {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: true
};
const NodePluginInput = ({ data }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { toast } = useToast();
const [editField, setEditField] = useState<EditFieldType>();
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<NodeCard minW={'300px'} {...data}>
@@ -42,10 +68,10 @@ const NodePluginInput = ({ data }: NodeProps<FlowModuleItemType>) => {
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
type: item.type,
inputType: item.type,
valueType: item.valueType,
key: item.key,
label: item.label,
valueType: item.valueType,
description: item.description,
required: item.required
})
@@ -71,14 +97,13 @@ const NodePluginInput = ({ data }: NodeProps<FlowModuleItemType>) => {
});
}}
/>
{item.description && (
<MyTooltip label={item.description} forceShow>
<MyTooltip label={t(item.description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box position={'relative'}>
{item.label}
{t(item.label)}
{item.required && (
<Box
position={'absolute'}
@@ -99,95 +124,126 @@ const NodePluginInput = ({ data }: NodeProps<FlowModuleItemType>) => {
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(defaultInputField);
setCreateField(defaultCreateField);
}}
>
{t('core.module.input.Add Input')}
</Button>
</Box>
</Container>
{!!editField && (
{!!createField && (
<FieldEditModal
mode={'pluginInput'}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(e) => {
// create field
if (e.createSign) {
// check key repeat
const memInput = inputs.find((item) => item.key === e.key);
if (memInput) {
return toast({
status: 'warning',
title: '字段key已存在'
});
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: createEditField
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
setCreateField(undefined);
}}
/>
)}
{!!editField?.key && (
<FieldEditModal
editField={createEditField}
defaultField={editField}
keys={[editField.key]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: e.key,
valueType: e.valueType,
type: e.type,
label: e.label,
required: e.required,
edit: true
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: e.key,
valueType: e.valueType,
label: e.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
return setEditField(undefined);
}
// check key valid
const memInput = inputs.find((item) => item.key === editField.key);
const memOutput = outputs.find((item) => item.key === editField.key);
if (!memInput || !memOutput) return setEditField(undefined);
const input = {
const newInput: FlowNodeInputItemType = {
...memInput,
...e
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description,
...(data.inputType === FlowNodeInputTypeEnum.addInputParam
? {
editField: {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: false
},
defaultEditField: {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
}
}
: {})
};
const output = {
const newOutput: FlowNodeOutputItemType = {
...memOutput,
...e
valueType: data.valueType,
key: data.key,
label: data.label
};
// not update key
if (editField.key === e.key) {
onChangeNode({
moduleId,
type: 'updateInput',
key: editField.key,
value: input
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: editField.key,
value: output
});
} else {
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: input
value: newInput
});
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: output
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}

View File

@@ -1,25 +1,52 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { onChangeNode } from '../../FlowProvider';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import Container from '../modules/Container';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import { EditFieldType, defaultOutputField } from '../modules/FieldEditModal';
import TargetHandle from '../render/TargetHandle';
import { useToast } from '@/web/common/hooks/useToast';
import {
EditNodeFieldType,
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/module/node/type';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
const FieldEditModal = dynamic(() => import('../render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
};
const createEditField = {
key: true,
name: true,
description: true,
required: false,
dataType: true,
inputType: false
};
const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { toast } = useToast();
const [editField, setEditField] = useState<EditFieldType>();
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<NodeCard minW={'300px'} {...data}>
@@ -36,7 +63,7 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
>
<TargetHandle handleKey={item.key} valueType={item.valueType} />
<Box position={'relative'}>
{item.label}
{t(item.label)}
<Box
position={'absolute'}
right={'-6px'}
@@ -47,7 +74,11 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
*
</Box>
</Box>
{item.description && (
<MyTooltip label={t(item.description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={2} />
</MyTooltip>
)}
<MyIcon
name={'settingLight'}
w={'14px'}
@@ -56,10 +87,12 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
inputType: item.type,
valueType: item.valueType,
key: item.key,
label: item.label,
valueType: item.valueType,
description: item.description
description: item.description,
required: item.required
})
}
/>
@@ -68,7 +101,7 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={3}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
@@ -84,12 +117,6 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
});
}}
/>
{item.description && (
<MyTooltip label={item.description} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
</Flex>
))}
<Box textAlign={'left'} mt={5}>
@@ -97,92 +124,106 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(defaultOutputField);
setCreateField(defaultCreateField);
}}
>
{t('core.module.output.Add Output')}
</Button>
</Box>
</Container>
{!!editField && (
{!!createField && (
<FieldEditModal
mode={'output'}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(e) => {
if (e.createSign) {
// check key repeat
const memInput = inputs.find((item) => item.key === e.key);
if (memInput) {
return toast({
status: 'warning',
title: '字段key已存在'
});
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: createEditField
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
setCreateField(undefined);
}}
/>
)}
{!!editField?.key && (
<FieldEditModal
editField={createEditField}
defaultField={editField}
keys={[editField.key]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: e.key,
valueType: e.valueType,
type: e.type,
label: e.label,
required: e.required,
edit: true
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: e.key,
valueType: e.valueType,
label: e.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
return setEditField(undefined);
}
// check key valid
const memInput = inputs.find((item) => item.key === editField.key);
const memOutput = outputs.find((item) => item.key === editField.key);
if (!memInput || !memOutput) return;
const input = {
if (!memInput || !memOutput) return setEditField(undefined);
const newInput: FlowNodeInputItemType = {
...memInput,
...e
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description
};
const output = {
const newOutput: FlowNodeOutputItemType = {
...memOutput,
...e
valueType: data.valueType,
key: data.key,
label: data.label
};
// not update key
if (editField.key === e.key) {
onChangeNode({
moduleId,
type: 'updateInput',
key: editField.key,
value: input
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: editField.key,
value: output
});
} else {
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: input
value: newInput
});
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: output
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderOutput from '../render/RenderOutput';
const QuestionInputNode = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
const { moduleId, outputs } = data;
return (
<NodeCard minW={'240px'} {...data}>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';

View File

@@ -24,7 +24,7 @@ import VariableEdit from '../modules/VariableEdit';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import Container from '../modules/Container';
import NodeCard from '../modules/NodeCard';
import NodeCard from '../render/NodeCard';
import type { VariableItemType } from '@fastgpt/global/core/module/type.d';
import QGSwitch from '@/components/core/module/Flow/components/modules/QGSwitch';
import TTSSelect from '@/components/core/module/Flow/components/modules/TTSSelect';

View File

@@ -3,7 +3,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import NodeCard from '../../modules/NodeCard';
import NodeCard from '../../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../../modules/Container';
import { VariableInputEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';

View File

@@ -0,0 +1,249 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@/components/MyModal';
import { DYNAMIC_INPUT_KEY, ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import MySelect from '@/components/Select';
import { FlowValueTypeMap } from '@/web/core/modules/constants/dataType';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { EditInputFieldMap, EditNodeFieldType } from '@fastgpt/global/core/module/node/type.d';
import { useToast } from '@/web/common/hooks/useToast';
const FieldEditModal = ({
editField = {
key: true,
name: true,
description: true,
dataType: true
},
defaultField,
keys = [],
onClose,
onSubmit
}: {
editField?: EditInputFieldMap;
defaultField: EditNodeFieldType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const isCreate = useMemo(() => !defaultField.key, [defaultField.key]);
const showDynamicInputSelect =
!keys.includes(DYNAMIC_INPUT_KEY) || defaultField.key === DYNAMIC_INPUT_KEY;
const inputTypeList = [
{
label: t('core.module.inputType.target'),
value: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.input'),
value: FlowNodeInputTypeEnum.input,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
valueType: ModuleIOValueTypeEnum.boolean
},
{
label: t('core.module.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
valueType: ModuleIOValueTypeEnum.selectDataset
},
...(showDynamicInputSelect
? [
{
label: t('core.module.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
valueType: ModuleIOValueTypeEnum.any
}
]
: [])
];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -2)
.map((item) => ({
label: t(item.label),
value: item.value
}));
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: defaultField
});
const [refresh, setRefresh] = useState(false);
const showDataTypeSelect = useMemo(() => {
if (!editField.dataType) return false;
const inputType = getValues('inputType');
const outputType = getValues('outputType');
if (inputType === FlowNodeInputTypeEnum.target) return true;
if (outputType === FlowNodeOutputTypeEnum.source) return true;
return false;
}, [editField.dataType, getValues, refresh]);
const showRequired = useMemo(() => {
const inputType = getValues('inputType');
const valueType = getValues('valueType');
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.required;
}, [editField.required, getValues, refresh]);
const showNameInput = useMemo(() => {
const inputType = getValues('inputType');
return editField.name;
}, [editField.name, getValues, refresh]);
const showKeyInput = useMemo(() => {
const inputType = getValues('inputType');
const valueType = getValues('valueType');
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, getValues, refresh]);
const showDescriptionInput = useMemo(() => {
const inputType = getValues('inputType');
return editField.description;
}, [editField.description, getValues, refresh]);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/module/extract.png"
title={t('core.module.edit.Field Edit')}
onClose={onClose}
>
<ModalBody overflow={'visible'}>
{/* input type select: target, input, textarea.... */}
{editField.inputType && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('core.module.Input Type')}</Box>
<MySelect
w={'288px'}
list={inputTypeList}
value={getValues('inputType')}
onchange={(e: string) => {
const type = e as `${FlowNodeInputTypeEnum}`;
const selectedItem = inputTypeList.find((item) => item.value === type);
setValue('inputType', type);
setValue('valueType', selectedItem?.valueType);
if (type === FlowNodeInputTypeEnum.selectDataset) {
setValue('label', selectedItem?.label);
} else if (type === FlowNodeInputTypeEnum.addInputParam) {
setValue('label', t('core.module.valueType.dynamicTargetInput'));
setValue('key', DYNAMIC_INPUT_KEY);
setValue('required', false);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{showRequired && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('common.Require Input')}</Box>
<Switch {...register('required')} />
</Flex>
)}
{showDataTypeSelect && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<MySelect
w={'288px'}
list={dataTypeSelectList}
value={getValues('valueType')}
onchange={(e: string) => {
const type = e as `${ModuleIOValueTypeEnum}`;
setValue('valueType', type);
if (
type === ModuleIOValueTypeEnum.chatHistory ||
type === ModuleIOValueTypeEnum.datasetQuote
) {
const label = dataTypeSelectList.find((item) => item.value === type)?.label;
setValue('label', label);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{showNameInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Name')}</Box>
<Input placeholder="预约字段/sql语句……" {...register('label', { required: true })} />
</Flex>
)}
{showKeyInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box>
<Input placeholder="appointment/sql" {...register('key', { required: true })} />
</Flex>
)}
{showDescriptionInput && (
<Flex mb={5} alignItems={'flex-start'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Description')}</Box>
<Textarea placeholder={t('common.choosable')} rows={3} {...register('description')} />
</Flex>
)}
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button
onClick={handleSubmit((data) => {
if (!data.key) return;
if (isCreate && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
onSubmit({
data,
changeKey: !keys.includes(data.key)
});
})}
>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -8,7 +8,11 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@/web/common/hooks/useToast';
import { useFlowProviderStore, onChangeNode } from '../../FlowProvider';
import {
useFlowProviderStore,
onChangeNode,
type useFlowProviderStoreType
} from '../../FlowProvider';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -23,20 +27,28 @@ type Props = FlowModuleItemType & {
isPreview?: boolean;
};
const NodeCard = (props: Props) => {
const NodeCard = (
props: Props & {
onCopyNode: useFlowProviderStoreType['onCopyNode'];
onResetNode: useFlowProviderStoreType['onResetNode'];
onDelNode: useFlowProviderStoreType['onDelNode'];
}
) => {
const { t } = useTranslation();
const {
children,
avatar = LOGO_ICON,
name = '未知模块',
name = t('core.module.template.UnKnow Module'),
intro,
minW = '300px',
moduleId,
flowType,
inputs,
isPreview
isPreview,
onCopyNode,
onResetNode,
onDelNode
} = props;
const { onCopyNode, onResetNode, onDelNode } = useFlowProviderStore();
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
const { setLoading } = useSystemStore();
@@ -147,10 +159,10 @@ const NodeCard = (props: Props) => {
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
<Avatar src={avatar} borderRadius={'md'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
{name}
{t(name)}
</Box>
{intro && (
<MyTooltip label={intro} forceShow>
<MyTooltip label={t(intro)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mb={'1px'} ml={1} />
</MyTooltip>
)}
@@ -186,4 +198,10 @@ const NodeCard = (props: Props) => {
);
};
export default React.memo(NodeCard);
export default React.memo(function (props: Props) {
const { onCopyNode, onResetNode, onDelNode } = useFlowProviderStore();
return (
<NodeCard {...props} onCopyNode={onCopyNode} onResetNode={onResetNode} onDelNode={onDelNode} />
);
});

View File

@@ -1,729 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import {
Box,
Textarea,
Input,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Flex,
useDisclosure,
Button,
useTheme,
Grid,
Switch
} from '@chakra-ui/react';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import dynamic from 'next/dynamic';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select';
import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip';
import TargetHandle from './TargetHandle';
import MyIcon from '@/components/Icon';
import { useTranslation } from 'next-i18next';
import type { AIChatModuleProps } from '@fastgpt/global/core/module/node/type.d';
import { chatModelList, cqModelList } from '@/web/common/system/staticData';
import { formatPrice } from '@fastgpt/global/support/wallet/bill/tools';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import type { SelectedDatasetType } from '@fastgpt/global/core/module/api.d';
import { useQuery } from '@tanstack/react-query';
import type { EditFieldModeType, EditFieldType } from '../modules/FieldEditModal';
import { feConfigs } from '@/web/common/system/staticData';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
const SelectAppModal = dynamic(() => import('../../SelectAppModal'));
const AIChatSettingsModal = dynamic(() => import('../../../AIChatSettingsModal'));
const DatasetSelectModal = dynamic(() => import('../../../DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('../../../DatasetParamsModal'));
export const Label = React.memo(function Label({
moduleId,
inputKey,
editFiledType = 'input',
...item
}: FlowNodeInputItemType & {
moduleId: string;
inputKey: string;
editFiledType?: EditFieldModeType;
}) {
const { t } = useTranslation();
const { mode } = useFlowProviderStore();
const {
required = false,
description,
edit,
label,
type,
valueType,
showTargetInApp,
showTargetInPlugin
} = item;
const [editField, setEditField] = useState<EditFieldType>();
const targetHandle = useMemo(() => {
if (type === FlowNodeInputTypeEnum.target) return true;
if (mode === 'app' && showTargetInApp) return true;
if (mode === 'plugin' && showTargetInPlugin) return true;
return false;
}, [mode, showTargetInApp, showTargetInPlugin, type]);
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{t(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>
{targetHandle && <TargetHandle handleKey={inputKey} valueType={valueType} />}
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
label: item.label,
type: item.type,
valueType: item.valueType,
required: item.required,
key: inputKey,
description: item.description
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField && (
<FieldEditModal
mode={editFiledType}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={(e) => {
const data = {
...item,
...e
};
// same key
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'updateInput',
key: data.key,
value: data
});
} else {
// diff key. del and add
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: data
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
});
const RenderInput = ({
flowInputList,
moduleId,
CustomComponent = {},
editFiledType
}: {
flowInputList: FlowNodeInputItemType[];
moduleId: string;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
editFiledType?: EditFieldModeType;
}) => {
const sortInputs = useMemo(
() =>
flowInputList
.filter((item) => !item.plusField || feConfigs.isPlus)
.sort((a, b) => (a.key === FlowNodeInputTypeEnum.switch ? -1 : 1)),
[flowInputList]
);
return (
<>
{sortInputs.map(
(item) =>
item.type !== FlowNodeInputTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
{!!item.label && (
<Label
editFiledType={editFiledType}
moduleId={moduleId}
inputKey={item.key}
{...item}
/>
)}
<Box mt={2} className={'nodrag'}>
{item.type === FlowNodeInputTypeEnum.numberInput && (
<NumberInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.input && (
<TextInputRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.switch && (
<SwitchRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.textarea && (
<TextareaRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.select && (
<SelectRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.slider && (
<SliderRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.selectApp && (
<SelectAppRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.aiSettings && (
<AISetting inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{[
FlowNodeInputTypeEnum.selectChatModel,
FlowNodeInputTypeEnum.selectCQModel
].includes(item.type as any) && (
<SelectAIModelRender inputs={sortInputs} item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.selectDataset && (
<SelectDatasetRender item={item} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.selectDatasetParamsModal && (
<SelectDatasetParamsRender item={item} inputs={sortInputs} moduleId={moduleId} />
)}
{item.type === FlowNodeInputTypeEnum.custom && CustomComponent[item.key] && (
<>{CustomComponent[item.key]({ ...item })}</>
)}
</Box>
</Box>
)
)}
</>
);
};
export default React.memo(RenderInput);
type RenderProps = {
inputs?: FlowNodeInputItemType[];
item: FlowNodeInputItemType;
moduleId: string;
};
const NumberInputRender = React.memo(function NumberInputRender({ item, moduleId }: RenderProps) {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
});
const TextInputRender = React.memo(function TextInputRender({ item, moduleId }: RenderProps) {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
});
const SwitchRender = React.memo(function SwitchRender({ item, moduleId }: RenderProps) {
return (
<Switch
size={'lg'}
isChecked={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.checked
}
});
}}
/>
);
});
const TextareaRender = React.memo(function TextareaRender({ item, moduleId }: RenderProps) {
return (
<Textarea
rows={5}
placeholder={item.placeholder}
resize={'both'}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
});
const SelectRender = React.memo(function SelectRender({ item, moduleId }: RenderProps) {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
});
const SliderRender = React.memo(function SliderRender({ item, moduleId }: RenderProps) {
return (
<Box pt={5} pb={4} px={2}>
<MySlider
markList={item.markList}
width={'100%'}
min={item.min || 0}
max={item.max}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
});
const AISetting = React.memo(function AISetting({ inputs = [], moduleId }: RenderProps) {
const { t } = useTranslation();
const chatModulesData = useMemo(() => {
const obj: Record<string, any> = {};
inputs.forEach((item) => {
obj[item.key] = item.value;
});
return obj as AIChatModuleProps;
}, [inputs]);
const {
isOpen: isOpenAIChatSetting,
onOpen: onOpenAIChatSetting,
onClose: onCloseAIChatSetting
} = useDisclosure();
return (
<>
<Button
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
onClick={onOpenAIChatSetting}
>
{t('app.AI Settings')}
</Button>
{isOpenAIChatSetting && (
<AIChatSettingsModal
isAdEdit
onClose={onCloseAIChatSetting}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
onCloseAIChatSetting();
}}
defaultData={chatModulesData}
/>
)}
</>
);
});
const SelectAIModelRender = React.memo(function SelectAIModelRender({
inputs = [],
item,
moduleId
}: RenderProps) {
const modelList = (() => {
if (item.type === FlowNodeInputTypeEnum.selectChatModel) return chatModelList;
if (item.type === FlowNodeInputTypeEnum.selectCQModel) return cqModelList;
return [];
})().map((item) => ({
model: item.model,
name: item.name,
maxResponse: item.maxResponse,
price: item.price
}));
const onChangeModel = useCallback(
(e: string) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
// update max tokens
const model = modelList.find((item) => item.model === e) || modelList[0];
if (!model) return;
onChangeNode({
moduleId,
type: 'updateInput',
key: 'maxToken',
value: {
...inputs.find((input) => input.key === 'maxToken'),
markList: [
{ label: '100', value: 100 },
{ label: `${model.maxResponse}`, value: model.maxResponse }
],
max: model.maxResponse,
value: model.maxResponse / 2
}
});
},
[inputs, item, modelList, moduleId]
);
const list = modelList.map((item) => {
const priceStr = `(${formatPrice(item.price, 1000)}元/1k Tokens)`;
return {
value: item.model,
label: `${item.name}${priceStr}`
};
});
useEffect(() => {
if (!item.value && list.length > 0) {
onChangeModel(list[0].value);
}
}, [item.value, list, onChangeModel]);
return (
<MySelect
minW={'350px'}
width={'100%'}
value={item.value}
list={list}
onchange={onChangeModel}
/>
);
});
const SelectDatasetRender = React.memo(function SelectDatasetRender({
item,
moduleId
}: RenderProps) {
const theme = useTheme();
const { mode } = useFlowProviderStore();
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const selectedDatasets = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value?.find((item) => item.datasetId === dataset._id));
}, [allDatasets, item.value]);
useQuery(['loadAllDatasets'], loadAllDatasets);
return (
<>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} minW={'350px'} w={'100%'}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
{selectedDatasets.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenKbSelect && (
<DatasetSelectModal
isOpen={isOpenKbSelect}
defaultSelectedDatasets={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseKbSelect}
/>
)}
</>
);
});
const SelectAppRender = React.memo(function SelectAppRender({ item, moduleId }: RenderProps) {
const { filterAppIds } = useFlowProviderStore();
const theme = useTheme();
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'base'} w={'100%'}>
</Button>
) : (
<Flex alignItems={'center'} border={theme.borders.base} borderRadius={'md'} px={3} py={2}>
<Avatar src={value?.logo} />
<Box fontWeight={'bold'} ml={1}>
{value?.name}
</Box>
</Flex>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: e[0]
}
});
}}
/>
)}
</>
);
});
const SelectDatasetParamsRender = React.memo(function SelectDatasetParamsRender({
item,
inputs = [],
moduleId
}: RenderProps) {
const { nodes } = useFlowProviderStore();
const { t } = useTranslation();
const [data, setData] = useState({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5
});
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodes.forEach((item) => {
if (item.type === FlowNodeTypeEnum.chatNode) {
const model =
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
chatModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [nodes]);
const { isOpen, onOpen, onClose } = useDisclosure();
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
return (
<>
<Button
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
onClick={onOpen}
>
{t('core.dataset.search.Params Setting')}
</Button>
{isOpen && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onClose}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)}
</>
);
});

View File

@@ -0,0 +1,160 @@
import { EditNodeFieldType, FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
onChangeNode,
useFlowProviderStore,
useFlowProviderStoreType
} from '../../../FlowProvider';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import TargetHandle from '../TargetHandle';
import MyIcon from '@/components/Icon';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
type Props = FlowNodeInputItemType & {
moduleId: string;
inputKey: string;
};
const InputLabel = ({
moduleId,
inputKey,
mode,
...item
}: Props & {
mode: useFlowProviderStoreType['mode'];
}) => {
const { t } = useTranslation();
const {
required = false,
description,
edit,
label,
type,
valueType,
showTargetInApp,
showTargetInPlugin
} = item;
const [editField, setEditField] = useState<EditNodeFieldType>();
const targetHandle = useMemo(() => {
if (type === FlowNodeInputTypeEnum.target) return true;
if (mode === 'app' && showTargetInApp) return true;
if (mode === 'plugin' && showTargetInPlugin) return true;
return false;
}, [mode, showTargetInApp, showTargetInPlugin, type]);
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{t(label)}
{description && (
<MyTooltip label={t(description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
{required && (
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
{targetHandle && <TargetHandle handleKey={inputKey} valueType={valueType} />}
{edit && (
<>
<MyIcon
name={'settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
inputType: type,
valueType: valueType,
key: inputKey,
required,
label,
description
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField?.key && (
<FieldEditModal
editField={item.editField}
keys={[editField.key]}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
const newInput: FlowNodeInputItemType = {
...item,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
export default React.memo(function (props: Props) {
const { mode } = useFlowProviderStore();
return <InputLabel {...props} mode={mode} />;
});

View File

@@ -0,0 +1,144 @@
import React, { useMemo } from 'react';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { Box } from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import InputLabel from './Label';
import type { RenderInputProps } from './type.d';
import { useFlowProviderStore, type useFlowProviderStoreType } from '../../../FlowProvider';
const RenderList: {
types: `${FlowNodeInputTypeEnum}`[];
Component: React.ComponentType<RenderInputProps>;
}[] = [
{
types: [FlowNodeInputTypeEnum.input],
Component: dynamic(() => import('./templates/TextInput'))
},
{
types: [FlowNodeInputTypeEnum.numberInput],
Component: dynamic(() => import('./templates/NumberInput'))
},
{
types: [FlowNodeInputTypeEnum.switch],
Component: dynamic(() => import('./templates/Switch'))
},
{
types: [FlowNodeInputTypeEnum.textarea],
Component: dynamic(() => import('./templates/Textarea'))
},
{
types: [FlowNodeInputTypeEnum.select],
Component: dynamic(() => import('./templates/Select'))
},
{
types: [FlowNodeInputTypeEnum.slider],
Component: dynamic(() => import('./templates/Slider'))
},
{
types: [FlowNodeInputTypeEnum.selectApp],
Component: dynamic(() => import('./templates/SelectApp'))
},
{
types: [FlowNodeInputTypeEnum.aiSettings],
Component: dynamic(() => import('./templates/AiSetting'))
},
{
types: [
FlowNodeInputTypeEnum.selectChatModel,
FlowNodeInputTypeEnum.selectCQModel,
FlowNodeInputTypeEnum.selectExtractModel
],
Component: dynamic(() => import('./templates/SelectAiModel'))
},
{
types: [FlowNodeInputTypeEnum.selectDataset],
Component: dynamic(() => import('./templates/SelectDataset'))
},
{
types: [FlowNodeInputTypeEnum.selectDatasetParamsModal],
Component: dynamic(() => import('./templates/SelectDatasetParams'))
},
{
types: [FlowNodeInputTypeEnum.addInputParam],
Component: dynamic(() => import('./templates/AddInputParam'))
}
];
type Props = {
flowInputList: FlowNodeInputItemType[];
moduleId: string;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
};
const RenderInput = ({
flowInputList,
moduleId,
CustomComponent = {},
mode
}: Props & {
mode: useFlowProviderStoreType['mode'];
}) => {
const sortInputs = useMemo(
() =>
flowInputList.sort((a, b) => {
if (a.type === FlowNodeInputTypeEnum.addInputParam) {
return 1;
}
if (b.type === FlowNodeInputTypeEnum.addInputParam) {
return -1;
}
if (a.type === FlowNodeInputTypeEnum.switch) {
return -1;
}
return 0;
}),
[flowInputList]
);
const filterInputs = useMemo(
() =>
sortInputs.filter((input) => {
if (mode === 'app' && input.hideInApp) return false;
if (mode === 'plugin' && input.hideInPlugin) return false;
return true;
}),
[mode, sortInputs]
);
return (
<>
{filterInputs.map((input) => {
const RenderComponent = (() => {
if (input.type === FlowNodeInputTypeEnum.custom && CustomComponent[input.key]) {
return <>{CustomComponent[input.key]({ ...input })}</>;
}
const Component = RenderList.find((item) => item.types.includes(input.type))?.Component;
if (!Component) return null;
return <Component inputs={filterInputs} item={input} moduleId={moduleId} />;
})();
return (
input.type !== FlowNodeInputTypeEnum.hidden && (
<Box key={input.key} _notLast={{ mb: 7 }} position={'relative'}>
{!!input.label && <InputLabel moduleId={moduleId} inputKey={input.key} {...input} />}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
)
);
})}
</>
);
};
export default React.memo(function (props: Props) {
const { mode } = useFlowProviderStore();
return <RenderInput {...props} mode={mode} />;
});

View File

@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { EditNodeFieldType } from '@fastgpt/global/core/module/node/type';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<>
<Button
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(item.defaultEditField || {});
}}
>
{t('core.module.input.Add Input')}
</Button>
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={inputs.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
key: data.key,
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: item.editField
}
});
setEditField(undefined);
}}
/>
)}
</>
);
};
export default React.memo(AddInputParam);

View File

@@ -0,0 +1,63 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import { Button, useDisclosure } from '@chakra-ui/react';
import { AIChatModuleProps } from '@fastgpt/global/core/module/node/type';
import MyIcon from '@/components/Icon';
import AIChatSettingsModal from '@/components/core/module/AIChatSettingsModal';
const AiSettingRender = ({ inputs = [], moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const chatModulesData = useMemo(() => {
const obj: Record<string, any> = {};
inputs.forEach((item) => {
obj[item.key] = item.value;
});
return obj as AIChatModuleProps;
}, [inputs]);
const {
isOpen: isOpenAIChatSetting,
onOpen: onOpenAIChatSetting,
onClose: onCloseAIChatSetting
} = useDisclosure();
return (
<>
<Button
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
onClick={onOpenAIChatSetting}
>
{t('app.AI Settings')}
</Button>
{isOpenAIChatSetting && (
<AIChatSettingsModal
isAdEdit
onClose={onCloseAIChatSetting}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
onCloseAIChatSetting();
}}
defaultData={chatModulesData}
/>
)}
</>
);
};
export default React.memo(AiSettingRender);

View File

@@ -0,0 +1,39 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper
} from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const NumberInputRender = ({ item, moduleId }: RenderInputProps) => {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
};
export default React.memo(NumberInputRender);

View File

@@ -0,0 +1,27 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import MySelect from '@/components/Select';
const SelectRender = ({ item, moduleId }: RenderInputProps) => {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
};
export default React.memo(SelectRender);

View File

@@ -0,0 +1,83 @@
import React, { useCallback, useEffect } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import MySelect from '@/components/Select';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { chatModelList, cqModelList, extractModelList } from '@/web/common/system/staticData';
import { formatPrice } from '@fastgpt/global/support/wallet/bill/tools';
const SelectAiModelRender = ({ item, inputs = [], moduleId }: RenderInputProps) => {
const modelList = (() => {
if (item.type === FlowNodeInputTypeEnum.selectChatModel) return chatModelList;
if (item.type === FlowNodeInputTypeEnum.selectCQModel) return cqModelList;
if (item.type === FlowNodeInputTypeEnum.selectExtractModel) return extractModelList;
return [];
})().map((item) => ({
model: item.model,
name: item.name,
maxResponse: item.maxResponse,
price: item.price
}));
const onChangeModel = useCallback(
(e: string) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
// update max tokens
const model = modelList.find((item) => item.model === e) || modelList[0];
if (!model) return;
onChangeNode({
moduleId,
type: 'updateInput',
key: 'maxToken',
value: {
...inputs.find((input) => input.key === 'maxToken'),
markList: [
{ label: '100', value: 100 },
{ label: `${model.maxResponse}`, value: model.maxResponse }
],
max: model.maxResponse,
value: model.maxResponse / 2
}
});
},
[inputs, item, modelList, moduleId]
);
const list = modelList.map((item) => {
const priceStr = `(${formatPrice(item.price, 1000)}元/1k Tokens)`;
return {
value: item.model,
label: `${item.name}${priceStr}`
};
});
useEffect(() => {
if (!item.value && list.length > 0) {
onChangeModel(list[0].value);
}
}, [item.value, list, onChangeModel]);
return (
<MySelect
minW={'350px'}
width={'100%'}
value={item.value}
list={list}
onchange={onChangeModel}
/>
);
};
export default React.memo(SelectAiModelRender);

View File

@@ -0,0 +1,72 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import {
onChangeNode,
useFlowProviderStore,
type useFlowProviderStoreType
} from '../../../../FlowProvider';
import { Box, Button, Flex, useDisclosure, useTheme } from '@chakra-ui/react';
import { SelectAppItemType } from '@fastgpt/global/core/module/type';
import Avatar from '@/components/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
const SelectAppRender = ({
item,
moduleId,
filterAppIds
}: RenderInputProps & {
filterAppIds: useFlowProviderStoreType['filterAppIds'];
}) => {
const theme = useTheme();
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'base'} w={'100%'}>
</Button>
) : (
<Flex alignItems={'center'} border={theme.borders.base} borderRadius={'md'} px={3} py={2}>
<Avatar src={value?.logo} />
<Box fontWeight={'bold'} ml={1}>
{value?.name}
</Box>
</Flex>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: e[0]
}
});
}}
/>
)}
</>
);
};
export default React.memo(function (props: RenderInputProps) {
const { filterAppIds } = useFlowProviderStore();
return <SelectAppRender {...props} filterAppIds={filterAppIds} />;
});

View File

@@ -0,0 +1,78 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@fastgpt/global/core/module/api';
import Avatar from '@/components/Avatar';
import DatasetSelectModal from '@/components/core/module/DatasetSelectModal';
import { useQuery } from '@tanstack/react-query';
const SelectDatasetRender = ({ item, moduleId }: RenderInputProps) => {
const theme = useTheme();
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const selectedDatasets = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value?.find((item) => item.datasetId === dataset._id));
}, [allDatasets, item.value]);
useQuery(['loadAllDatasets'], loadAllDatasets);
return (
<>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={4} minW={'350px'} w={'100%'}>
<Button h={'36px'} onClick={onOpenKbSelect}>
</Button>
{selectedDatasets.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenKbSelect && (
<DatasetSelectModal
isOpen={isOpenKbSelect}
defaultSelectedDatasets={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseKbSelect}
/>
)}
</>
);
};
export default React.memo(SelectDatasetRender);

View File

@@ -0,0 +1,90 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode, useFlowProviderStore } from '../../../../FlowProvider';
import { Button, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { chatModelList } from '@/web/common/system/staticData';
import MyIcon from '@/components/Icon';
import DatasetParamsModal from '@/components/core/module/DatasetParamsModal';
const SelectDatasetParam = ({ inputs = [], moduleId }: RenderInputProps) => {
const { nodes } = useFlowProviderStore();
const { t } = useTranslation();
const [data, setData] = useState({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5
});
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodes.forEach((item) => {
if (item.type === FlowNodeTypeEnum.chatNode) {
const model =
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
chatModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [nodes]);
const { isOpen, onOpen, onClose } = useDisclosure();
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
return (
<>
<Button
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'14px'} />}
onClick={onOpen}
>
{t('core.dataset.search.Params Setting')}
</Button>
{isOpen && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onClose}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)}
</>
);
};
export default React.memo(SelectDatasetParam);

View File

@@ -0,0 +1,35 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import MySlider from '@/components/Slider';
const SliderRender = ({ item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
return (
<Box pt={5} pb={4} px={2}>
<MySlider
markList={item.markList}
width={'100%'}
min={item.min || 0}
max={item.max}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
};
export default React.memo(SliderRender);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Switch } from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const SwitchRender = ({ item, moduleId }: RenderInputProps) => {
return (
<Switch
size={'lg'}
isChecked={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.checked
}
});
}}
/>
);
};
export default React.memo(SwitchRender);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Input } from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const TextInput = ({ item, moduleId }: RenderInputProps) => {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
};
export default React.memo(TextInput);

View File

@@ -0,0 +1,32 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import PromptTextarea from '@/components/common/Textarea/PromptTextarea';
const TextareaRender = ({ item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
return (
<PromptTextarea
title={t(item.label)}
rows={5}
bg={'myWhite.400'}
placeholder={t(item.placeholder || '')}
resize={'both'}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
};
export default React.memo(TextareaRender);

View File

@@ -0,0 +1,7 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
export type RenderInputProps = {
inputs?: FlowNodeInputItemType[];
item: FlowNodeInputItemType;
moduleId: string;
};

View File

@@ -1,34 +1,30 @@
import React, { useMemo, useState } from 'react';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { Box, Flex } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from './SourceHandle';
import MyIcon from '@/components/Icon';
import dynamic from 'next/dynamic';
import { onChangeNode } from '../../FlowProvider';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import { EditNodeFieldType, FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
import { onChangeNode } from '../../../FlowProvider';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import SourceHandle from '../SourceHandle';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import type { EditFieldType, EditFieldModeType } from '../modules/FieldEditModal';
const FieldEditModal = dynamic(() => import('../modules/FieldEditModal'));
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
export const Label = ({
const OutputLabel = ({
moduleId,
outputKey,
outputs,
editFiledType = 'output',
...item
}: FlowNodeOutputItemType & {
outputKey: string;
moduleId: string;
outputs: FlowNodeOutputItemType[];
editFiledType?: EditFieldModeType;
}) => {
const { t } = useTranslation();
const { label = '', description, edit } = item;
const [editField, setEditField] = useState<EditFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<Flex
@@ -48,10 +44,11 @@ export const Label = ({
_hover={{ color: 'myBlue.600' }}
onClick={() =>
setEditField({
label: item.label,
valueType: item.valueType,
key: outputKey,
description: item.description
label: item.label,
description: item.description,
valueType: item.valueType,
outputType: item.type
})
}
/>
@@ -79,29 +76,41 @@ export const Label = ({
)}
<Box>{t(label)}</Box>
{item.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle handleKey={outputKey} valueType={item.valueType} />
)}
{!!editField && (
<FieldEditModal
mode={editFiledType}
editField={item.editField}
defaultField={editField}
keys={[outputKey]}
onClose={() => setEditField(undefined)}
onSubmit={(e) => {
const data = {
onSubmit={({ data, changeKey }) => {
if (!data.outputType || !data.key) return;
const newOutput: FlowNodeOutputItemType = {
...item,
...e
type: data.outputType,
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description
};
if (editField.key === data.key) {
onChangeNode({
moduleId,
type: 'updateOutput',
key: data.key,
value: data
});
} else {
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: data
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
@@ -113,48 +122,4 @@ export const Label = ({
);
};
const RenderOutput = ({
moduleId,
flowOutputList,
editFiledType
}: {
moduleId: string;
flowOutputList: FlowNodeOutputItemType[];
editFiledType?: EditFieldModeType;
}) => {
const sortOutput = useMemo(
() =>
[...flowOutputList].sort((a, b) => {
if (a.key === ModuleOutputKeyEnum.finish) return -1;
if (b.key === ModuleOutputKeyEnum.finish) return 1;
return 0;
}),
[flowOutputList]
);
return (
<>
{sortOutput.map(
(item) =>
item.type !== FlowNodeOutputTypeEnum.hidden && (
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
<Label
editFiledType={editFiledType}
moduleId={moduleId}
outputKey={item.key}
outputs={sortOutput}
{...item}
/>
<Box mt={FlowNodeOutputTypeEnum.answer ? 0 : 2} className={'nodrag'}>
{item.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle handleKey={item.key} valueType={item.valueType} />
)}
</Box>
</Box>
)
)}
</>
);
};
export default React.memo(RenderOutput);
export default React.memo(OutputLabel);

View File

@@ -0,0 +1,80 @@
import React, { useMemo } from 'react';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { Box } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import OutputLabel from './Label';
import { RenderOutputProps } from './type';
import dynamic from 'next/dynamic';
const RenderList: {
types: `${FlowNodeOutputTypeEnum}`[];
Component: React.ComponentType<RenderOutputProps>;
}[] = [
{
types: [FlowNodeOutputTypeEnum.addOutputParam],
Component: dynamic(() => import('./templates/AddOutputParam'))
}
];
const RenderOutput = ({
moduleId,
flowOutputList
}: {
moduleId: string;
flowOutputList: FlowNodeOutputItemType[];
}) => {
const sortOutputs = useMemo(
() =>
[...flowOutputList].sort((a, b) => {
if (a.type === FlowNodeOutputTypeEnum.addOutputParam) {
return 1;
}
if (b.type === FlowNodeOutputTypeEnum.addOutputParam) {
return -1;
}
if (a.key === ModuleOutputKeyEnum.finish) return -1;
if (b.key === ModuleOutputKeyEnum.finish) return 1;
return 0;
}),
[flowOutputList]
);
return (
<>
{sortOutputs.map((output) => {
const RenderComponent = (() => {
const Component = RenderList.find(
(item) => output.type && item.types.includes(output.type)
)?.Component;
if (!Component) return null;
return <Component outputs={sortOutputs} item={output} moduleId={moduleId} />;
})();
return (
output.type !== FlowNodeOutputTypeEnum.hidden && (
<Box key={output.key} _notLast={{ mb: 7 }} position={'relative'}>
{output.label && (
<OutputLabel
moduleId={moduleId}
outputKey={output.key}
outputs={sortOutputs}
{...output}
/>
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
)
);
})}
</>
);
};
export default React.memo(RenderOutput);

View File

@@ -0,0 +1,59 @@
import React, { useState } from 'react';
import type { RenderOutputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Box, Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/module/node/type';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddOutputParam = ({ outputs = [], item, moduleId }: RenderOutputProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<Box textAlign={'right'}>
<Button
variant={'base'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(item.defaultEditField || {});
}}
>
{t('core.module.output.Add Output')}
</Button>
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={outputs.map((output) => output.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addOutput',
key: data.key,
value: {
type: data.outputType,
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description,
required: data.required,
edit: true,
editField: item.editField,
targets: []
}
});
setEditField(undefined);
}}
/>
)}
</Box>
);
};
export default React.memo(AddOutputParam);

View File

@@ -0,0 +1,7 @@
import { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
export type RenderOutputProps = {
outputs?: FlowNodeOutputItemType[];
item: FlowNodeOutputItemType;
moduleId: string;
};

View File

@@ -1,26 +1,26 @@
import React, { useMemo, useTransition } from 'react';
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, Position } from 'reactflow';
import { FlowValueTypeStyle, FlowValueTypeMap } from '@/web/core/modules/constants/dataType';
import { FlowValueTypeMap } from '@/web/core/modules/constants/dataType';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${ModuleDataTypeEnum}`;
valueType?: `${ModuleIOValueTypeEnum}`;
}
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? ModuleDataTypeEnum.any;
const valType = valueType ?? ModuleIOValueTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[ModuleDataTypeEnum.any] as any),
valueType && FlowValueTypeMap[valueType]
? FlowValueTypeMap[valueType]?.handlerStyle
: FlowValueTypeMap[ModuleIOValueTypeEnum.any]?.handlerStyle,
[valueType]
);
@@ -34,8 +34,8 @@ const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeMap[valType].label),
example: FlowValueTypeMap[valType].example
type: t(FlowValueTypeMap[valType]?.label),
description: FlowValueTypeMap[valType]?.description
})}
>
<Handle

View File

@@ -1,32 +1,31 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, OnConnect, Position } from 'reactflow';
import { FlowValueTypeStyle, FlowValueTypeMap } from '@/web/core/modules/constants/dataType';
import { FlowValueTypeMap } from '@/web/core/modules/constants/dataType';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${ModuleDataTypeEnum}`;
valueType?: `${ModuleIOValueTypeEnum}`;
onConnect?: OnConnect;
}
const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? ModuleDataTypeEnum.any;
const valType = valueType ?? ModuleIOValueTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType
? FlowValueTypeStyle[valueType]
: (FlowValueTypeStyle[ModuleDataTypeEnum.any] as any),
valueType && FlowValueTypeMap[valueType]
? FlowValueTypeMap[valueType]?.handlerStyle
: FlowValueTypeMap[ModuleIOValueTypeEnum.any]?.handlerStyle,
[valueType]
);
return (
<Box
key={handleKey}
position={'absolute'}
top={'50%'}
left={'-16px'}
@@ -35,8 +34,8 @@ const TargetHandle = ({ handleKey, valueType, onConnect, ...props }: Props) => {
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeMap[valType].label),
example: FlowValueTypeMap[valType].example
type: t(FlowValueTypeMap[valType]?.label),
description: FlowValueTypeMap[valType]?.description
})}
>
<Handle

View File

@@ -2,8 +2,7 @@ import React, { useEffect } from 'react';
import ReactFlow, { Background, Controls, ReactFlowProvider } from 'reactflow';
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { edgeOptions, connectionLineStyle } from '@/web/core/modules/constants/flowUi';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
@@ -25,14 +24,14 @@ const nodeTypes = {
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./components/nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./components/nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./components/nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest]: dynamic(() => import('./components/nodes/NodeHttp')),
[FlowNodeTypeEnum.httpRequest]: NodeSimple,
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./components/nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./components/nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple
};
const edgeTypes = {
buttonedge: ButtonEdge
[EDGE_TYPE]: ButtonEdge
};
type Props = {
modules: ModuleItemType[];
@@ -40,7 +39,7 @@ type Props = {
} & ModuleTemplateProps;
const Container = React.memo(function Container(props: Props) {
const { modules = [], Header, systemTemplates, pluginTemplates } = props;
const { modules = [], Header, templates } = props;
const {
isOpen: isOpenTemplate,
@@ -96,8 +95,10 @@ const Container = React.memo(function Container(props: Props) {
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={edgeOptions}
connectionLineStyle={connectionLineStyle}
defaultEdgeOptions={{
animated: true
}}
connectionLineStyle={{ strokeWidth: 2, stroke: '#5A646Es' }}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
@@ -115,8 +116,7 @@ const Container = React.memo(function Container(props: Props) {
</ReactFlow>
<ModuleTemplateList
systemTemplates={systemTemplates}
pluginTemplates={pluginTemplates}
templates={templates}
isOpen={isOpenTemplate}
onClose={onCloseTemplate}
/>

View File

@@ -0,0 +1,51 @@
import { FlowNodeOutputTargetItemType } from '@fastgpt/global/core/module/node/type';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { type Node, type Edge } from 'reactflow';
export function flowNode2Modules({
nodes,
edges
}: {
nodes: Node<FlowModuleItemType, string | undefined>[];
edges: Edge<any>[];
}) {
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
avatar: item.data.avatar,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((input) => ({
...input,
connected: false
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowNodeOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected = !!edges.find(
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter(
(edge) =>
edge.source === module.moduleId && edge.sourceHandle === output.key && edge.targetHandle
)
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
}

View File

@@ -25,7 +25,7 @@ export const Prompt_ExtractJson = `你可以从 <对话记录></对话记录>
<字段说明>
1. 下面的 JSON 字符串均按照 JSON Schema 的规则描述。
2. key 代表字段名description 代表字段的描述required 代表字段是否必须。
2. key 代表字段名description 代表字段的描述required 代表字段是否必须enum 是可选值,代表可选的 value
3. 如果字段内容为空,你可以返回空字符串。
{{json}}

View File

@@ -6,7 +6,7 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constant';
import { ModuleDataTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
let success = 0;
@@ -52,7 +52,7 @@ export async function initApp(limit = 50): Promise<any> {
key: ModuleInputKeyEnum.datasetSearchMode,
type: FlowNodeInputTypeEnum.hidden,
label: 'core.dataset.search.Mode',
valueType: ModuleDataTypeEnum.string,
valueType: ModuleIOValueTypeEnum.string,
showTargetInApp: false,
showTargetInPlugin: false,
value: val

View File

@@ -7,7 +7,7 @@ import { jsonRes } from '@fastgpt/service/common/response';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import type { ModuleItemType } from '@fastgpt/global/core/module/type';
import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type.d';
import { FormatForm2ModulesProps } from '@fastgpt/global/core/app/api';
@@ -317,7 +317,7 @@ function datasetTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
key: ModuleInputKeyEnum.answerText,
value: formData.dataset.searchEmptyText,
type: FlowNodeInputTypeEnum.textarea,
valueType: ModuleDataTypeEnum.string,
valueType: ModuleIOValueTypeEnum.string,
label: '回复的内容',
connected: true
}

View File

@@ -6,11 +6,10 @@ import { addLog } from '@fastgpt/service/common/system/log';
import { authDataset } from '@fastgpt/service/support/permission/auth/dataset';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { findDatasetIdTreeByTopDatasetId } from '@fastgpt/service/core/dataset/controller';
import { Readable } from 'stream';
import type { Cursor } from '@fastgpt/service/common/mongo';
import { limitCheck } from './checkExportLimit';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
let { datasetId } = req.query as {
@@ -81,7 +80,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
error: err
});
}
}
});
export const config = {
api: {

View File

@@ -33,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
})),
...(global.communityPlugins?.map((plugin) => ({
id: plugin.id,
templateType: ModuleTemplateTypeEnum.communityPlugin,
templateType: plugin.templateType ?? ModuleTemplateTypeEnum.other,
flowType: FlowNodeTypeEnum.pluginModule,
avatar: plugin.avatar,
name: plugin.name,

View File

@@ -0,0 +1,69 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
type Props = HttpBodyType<{
input: string;
rule?: string;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
data: { input, rule = '' }
} = req.body as Props;
const result = (() => {
if (typeof input === 'string') {
const defaultReg: any[] = [
undefined,
'undefined',
null,
'null',
false,
'false',
0,
'0',
'none'
];
const customReg = rule.split('\n');
defaultReg.push(...customReg);
return !defaultReg.find((item) => {
const reg = typeof item === 'string' ? stringToRegex(item) : null;
if (reg) {
return reg.test(input);
}
return input === item;
});
}
return !!input;
})();
res.json({
...(result
? {
true: true
}
: {
false: false
})
});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));
}
}
function stringToRegex(str: string) {
const regexFormat = /^\/(.+)\/([gimuy]*)$/;
const match = str.match(regexFormat);
if (match) {
const [, pattern, flags] = match;
return new RegExp(pattern, flags);
} else {
return null;
}
}

View File

@@ -0,0 +1,26 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
type Props = HttpBodyType<{
text: string;
[key: string]: any;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
data: { text, ...obj }
} = req.body as Props;
const textResult = replaceVariable(text, obj);
res.json({
text: textResult
});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));
}
}

View File

@@ -19,9 +19,10 @@ import {
} from '@fastgpt/global/core/ai/model';
import { SimpleModeTemplate_FastGPT_Universal } from '@/global/core/app/constants';
import { getSimpleTemplatesFromPlus } from '@/service/core/app/utils';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { getFastGPTFeConfig } from '@fastgpt/service/common/system/config/controller';
import { connectToDatabase } from '@/service/mongo';
import { PluginTemplateType } from '@fastgpt/global/core/plugin/type';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await getInitConfig();
@@ -251,12 +252,12 @@ function getSystemPlugin() {
const filterFiles = files.filter((item) => item.endsWith('.json'));
// read json file
const fileTemplates = filterFiles.map((item) => {
const content = readFileSync(`${basePath}/${item}`, 'utf-8');
const fileTemplates: PluginTemplateType[] = filterFiles.map((filename) => {
const content = readFileSync(`${basePath}/${filename}`, 'utf-8');
return {
id: `${PluginTypeEnum.community}-${item.replace('.json', '')}`,
type: PluginTypeEnum.community,
...JSON.parse(content)
...JSON.parse(content),
id: `${PluginSourceEnum.community}-${filename.replace('.json', '')}`,
source: PluginSourceEnum.community
};
});

View File

@@ -138,6 +138,11 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
// openapi key
if (authType === AuthUserTypeEnum.apikey) {
if (!apiKeyAppId) {
return Promise.reject(
'Key is error. You need to use the app key rather than the account key.'
);
}
const app = await MongoApp.findById(apiKeyAppId);
if (!app) {

View File

@@ -27,7 +27,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const { total } = pushReRankBill({
teamId,
tmbId,
source: 'api'
source: 'api',
inputs
});
if (apikey) {

View File

@@ -12,9 +12,11 @@ import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/module/Flow/ChatTest';
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { flowNode2Modules } from '@/components/core/module/utils';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { useToast } from '@/web/common/hooks/useToast';
import { useConfirm } from '@/web/common/hooks/useConfirm';
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
@@ -35,42 +37,47 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const { toast } = useToast();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { openConfirm: openConfirmOut, ConfirmModal } = useConfirm({
content: t('core.app.edit.Out Ad Edit')
});
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { updateAppDetail } = useAppStore();
const { nodes, edges, onFixView } = useFlowProviderStore();
const { nodes, edges } = useFlowProviderStore();
const flow2ModulesAndCheck = useCallback(
(tip = false) => {
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
if (
item.inputs.find((input) => {
if (!input.required || input.connected) return false;
if (!input.value || input.value === '' || input.value?.length === 0) return true;
return false;
})
) {
const msg = `${item.name}】存在未填或未连接参数`;
tip &&
toast({
status: 'warning',
title: msg
});
return Promise.reject(msg);
const flow2ModulesAndCheck = useCallback(() => {
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
const unconnected = item.inputs.find((input) => {
if (!input.required || input.connected) {
return false;
}
if (input.value === undefined || input.value === '' || input.value?.length === 0) {
return true;
}
return false;
});
if (unconnected) {
const msg = `${item.name}】存在未填或未连接参数`;
toast({
status: 'warning',
title: msg
});
return false;
}
return modules;
},
[edges, nodes, toast]
);
}
return modules;
}, [edges, nodes, toast]);
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: async () => {
mutationFn: async (modules: ModuleItemType[]) => {
return updateAppDetail(app._id, {
modules: await flow2ModulesAndCheck(),
modules: modules,
type: AppTypeEnum.advanced,
permission: undefined
});
@@ -91,7 +98,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
alignItems={'center'}
userSelect={'none'}
>
<MyTooltip label={'返回'} offset={[10, 10]}>
<MyTooltip label={t('common.Back')} offset={[10, 10]}>
<IconButton
size={'sm'}
icon={<MyIcon name={'back'} w={'14px'} />}
@@ -99,10 +106,13 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
borderColor={'myGray.300'}
variant={'base'}
aria-label={''}
onClick={() => {
onClick={openConfirmOut(async () => {
const modules = flow2ModulesAndCheck();
if (modules) {
await onclickSave(modules);
}
onClose();
onFixView();
}}
}, onClose)}
/>
</MyTooltip>
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
@@ -153,8 +163,11 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
borderRadius={'lg'}
aria-label={'save'}
variant={'base'}
onClick={async () => {
setTestModules(await flow2ModulesAndCheck(true));
onClick={() => {
const modules = flow2ModulesAndCheck();
if (modules) {
setTestModules(modules);
}
}}
/>
</MyTooltip>
@@ -166,11 +179,20 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
borderRadius={'lg'}
isLoading={isLoading}
aria-label={'save'}
onClick={onclickSave}
onClick={() => {
const modules = flow2ModulesAndCheck();
if (modules) {
onclickSave(modules);
}
}}
/>
</MyTooltip>
</Flex>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
<ConfirmModal
closeText={t('core.app.edit.UnSave')}
confirmText={t('core.app.edit.Save and out')}
/>
</>
);
});

View File

@@ -15,16 +15,16 @@ const Render = ({ app, onClose }: Props) => {
const { nodes } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginTemplates } = usePluginStore();
const filterTemplates = useMemo(() => {
const copyTemplates: FlowModuleTemplateType[] = JSON.parse(
JSON.stringify(appSystemModuleTemplates)
);
const moduleTemplates = useMemo(() => {
const concatTemplates = [...appSystemModuleTemplates, ...pluginModuleTemplates];
const copyTemplates: FlowModuleTemplateType[] = JSON.parse(JSON.stringify(concatTemplates));
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1
};
// filter some template
// filter some template, There can only be one
nodes.forEach((node) => {
if (node.type && filterType[node.type]) {
copyTemplates.forEach((module, index) => {
@@ -36,14 +36,13 @@ const Render = ({ app, onClose }: Props) => {
});
return copyTemplates;
}, [nodes]);
}, [nodes, pluginModuleTemplates]);
useQuery(['getPlugTemplates'], () => loadPluginTemplates());
return (
<Flow
systemTemplates={filterTemplates}
pluginTemplates={pluginModuleTemplates}
templates={moduleTemplates}
modules={app.modules}
Header={<Header app={app} onClose={onClose} />}
/>

View File

@@ -48,15 +48,18 @@ import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { SimpleModeTemplate_FastGPT_Universal } from '@/global/core/app/constants';
import QGSwitch from '@/components/core/module/Flow/components/modules/QGSwitch';
import TTSSelect from '@/components/core/module/Flow/components/modules/TTSSelect';
import VariableEdit from '@/components/core/module/Flow/components/modules/VariableEdit';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import PromptTextarea from '@/components/common/Textarea/PromptTextarea/index';
const InfoModal = dynamic(() => import('../InfoModal'));
const DatasetSelectModal = dynamic(() => import('@/components/core/module/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/module/DatasetParamsModal'));
const AIChatSettingsModal = dynamic(() => import('@/components/core/module/AIChatSettingsModal'));
const TTSSelect = dynamic(
() => import('@/components/core/module/Flow/components/modules/TTSSelect')
);
const QGSwitch = dynamic(() => import('@/components/core/module/Flow/components/modules/QGSwitch'));
function ConfigForm({
divRef,
@@ -100,8 +103,7 @@ function ConfigForm({
} = useDisclosure();
const { openConfirm: openConfirmSave, ConfirmModal: ConfirmSaveModal } = useConfirm({
content: t('app.Confirm Save App Tip'),
bg: appDetail.type === AppTypeEnum.simple ? '' : 'red.600'
content: t('core.app.edit.Confirm Save App Tip')
});
const chatModelSelectList = useMemo(() => {
@@ -330,13 +332,18 @@ function ConfigForm({
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Box>
<Textarea
<PromptTextarea
flex={1}
bg={'myWhite.400'}
rows={5}
minH={'60px'}
placeholder={chatNodeSystemPromptTip}
borderColor={'myGray.100'}
{...register('aiSettings.systemPrompt')}
></Textarea>
showSetModalModeIcon
value={getValues('aiSettings.systemPrompt')}
onChange={(e) => {
setValue('aiSettings.systemPrompt', e.target.value || '');
setRefresh(!refresh);
}}
/>
</Flex>
)}
</Box>
@@ -438,7 +445,7 @@ function ConfigForm({
)}
</Box>
<ConfirmSaveModal />
<ConfirmSaveModal bg={appDetail.type === AppTypeEnum.simple ? '' : 'red.600'} countDown={5} />
{isOpenAIChatSetting && (
<AIChatSettingsModal
onClose={onCloseAIChatSetting}

View File

@@ -26,7 +26,7 @@ import { POST } from '@/web/common/api/request';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
import { useChatStore } from '@/web/core/chat/storeChat';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import MyBox from '@/components/common/MyBox';
const OutLink = ({
shareId,
@@ -44,10 +44,10 @@ const OutLink = ({
const { toast } = useToast();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { isPc } = useSystemStore();
const forbidRefresh = useRef(false);
const [isEmbed, setIdEmbed] = useState(true);
const ChatBoxRef = useRef<ComponentRef>(null);
const forbidRefresh = useRef(false);
const initSign = useRef(false);
const [isEmbed, setIdEmbed] = useState(true);
const {
localUId,
@@ -161,7 +161,7 @@ const OutLink = ({
const res = await getInitOutLinkChatInfo({
chatId,
shareId,
outLinkUid: authToken || localUId
outLinkUid
});
const history = res.history.map((item) => ({
...item,
@@ -176,12 +176,21 @@ const OutLink = ({
ChatBoxRef.current?.resetHistory(history);
ChatBoxRef.current?.resetVariables(res.variables);
if (res.history.length > 0) {
// send init message
if (!initSign.current) {
initSign.current = true;
if (window !== top) {
window.top?.postMessage({ type: 'shareChatReady' }, '*');
}
}
if (chatId && res.history.length > 0) {
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
}
} catch (e: any) {
console.log(e);
toast({
status: 'error',
title: getErrText(e, t('core.shareChat.Init Error'))
@@ -194,16 +203,14 @@ const OutLink = ({
}
});
}
if (e?.statusText === OutLinkErrEnum.linkUnInvalid) {
router.replace('/');
}
}
return null;
},
[authToken, localUId, router, setChatData, t, toast]
[outLinkUid, router, setChatData, t, toast]
);
useQuery(['init', shareId, chatId], () => {
const { isFetching } = useQuery(['init', shareId, chatId], () => {
if (forbidRefresh.current) {
forbidRefresh.current = false;
return null;
@@ -223,11 +230,8 @@ const OutLink = ({
return null;
});
// check is embed
// window init
useEffect(() => {
if (window !== top) {
window.top?.postMessage({ type: 'shareChatReady' }, '*');
}
setIdEmbed(window !== top);
}, []);
@@ -258,7 +262,7 @@ const OutLink = ({
<Head>
<title>{chatData.app.name}</title>
</Head>
<Flex h={'100%'} flexDirection={['column', 'row']}>
<MyBox isLoading={isFetching} h={'100%'} display={'flex'} flexDirection={['column', 'row']}>
{showHistory === '1'
? ((children: React.ReactNode) => {
return isPc ? (
@@ -372,7 +376,7 @@ const OutLink = ({
/>
</Box>
</Flex>
</Flex>
</MyBox>
</PageContainer>
);
};

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { useRequest } from '@/web/common/hooks/useRequest';
@@ -7,10 +7,12 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { flowNode2Modules } from '@/components/core/module/utils';
import { putUpdatePlugin } from '@/web/core/plugin/api';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { useToast } from '@/web/common/hooks/useToast';
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
const PreviewPlugin = dynamic(() => import('./Preview'));
@@ -20,58 +22,83 @@ type Props = { plugin: PluginItemSchema; onClose: () => void };
const Header = ({ plugin, onClose }: Props) => {
const theme = useTheme();
const { t } = useTranslation();
const { toast } = useToast();
const { copyData } = useCopyData();
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { nodes, edges, onFixView } = useFlowProviderStore();
const [previewModules, setPreviewModules] = React.useState<ModuleItemType[]>();
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: () => {
const modules = flowNode2Modules({ nodes, edges });
const flow2ModulesAndCheck = useCallback(() => {
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
// update custom input connected
if (item.flowType === FlowNodeTypeEnum.pluginInput) {
item.inputs.forEach((item) => {
item.connected = true;
// update custom input connected
if (item.flowType === FlowNodeTypeEnum.pluginInput) {
item.inputs.forEach((item) => {
item.connected = true;
});
if (item.outputs.find((output) => output.targets.length === 0)) {
toast({
status: 'warning',
title: t('module.Plugin input must connect')
});
if (item.outputs.find((output) => output.targets.length === 0)) {
return Promise.reject(t('module.Plugin input must connect'));
}
}
if (
item.flowType === FlowNodeTypeEnum.pluginOutput &&
item.inputs.find((input) => !input.connected)
) {
return Promise.reject(t('core.module.Plugin output must connect'));
}
if (
item.inputs.find((input) => {
if (!input.required || input.connected) return false;
if (!input.value || input.value === '' || input.value?.length === 0) return true;
return false;
})
) {
return Promise.reject(`${item.name}】存在未填或未连接参数`);
return false;
}
}
// plugin must have input
const pluginInputModule = modules.find(
(item) => item.flowType === FlowNodeTypeEnum.pluginInput
);
if (!pluginInputModule) {
return Promise.reject(t('module.Plugin input is required'));
}
if (pluginInputModule.inputs.length < 1) {
return Promise.reject(t('module.Plugin input is not value'));
if (
item.flowType === FlowNodeTypeEnum.pluginOutput &&
item.inputs.find((input) => !input.connected)
) {
toast({
status: 'warning',
title: t('core.module.Plugin output must connect')
});
return false;
}
if (
item.inputs.find((input) => {
if (!input.required || input.connected) return false;
if (!input.value || input.value === '' || input.value?.length === 0) return true;
return false;
})
) {
toast({
status: 'warning',
title: `${item.name}】存在未填或未连接参数`
});
return false;
}
}
// plugin must have input
const pluginInputModule = modules.find(
(item) => item.flowType === FlowNodeTypeEnum.pluginInput
);
if (!pluginInputModule) {
toast({
status: 'warning',
title: t('module.Plugin input is required')
});
return false;
}
if (pluginInputModule.inputs.length < 1) {
toast({
status: 'warning',
title: t('module.Plugin input is not value')
});
return false;
}
return modules;
}, [edges, nodes, t, toast]);
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: (modules: ModuleItemType[]) => {
return putUpdatePlugin({
id: plugin._id,
modules
@@ -90,7 +117,7 @@ const Header = ({ plugin, onClose }: Props) => {
alignItems={'center'}
userSelect={'none'}
>
<MyTooltip label={'返回'} offset={[10, 10]}>
<MyTooltip label={t('common.Back')} offset={[10, 10]}>
<IconButton
size={'sm'}
icon={<MyIcon name={'back'} w={'14px'} />}
@@ -125,12 +152,12 @@ const Header = ({ plugin, onClose }: Props) => {
borderRadius={'lg'}
variant={'base'}
aria-label={'save'}
onClick={() =>
copyData(
JSON.stringify(flowNode2Modules({ nodes, edges }), null, 2),
t('app.Export Config Successful')
)
}
onClick={() => {
const modules = flow2ModulesAndCheck();
if (modules) {
copyData(JSON.stringify(modules, null, 2), t('app.Export Config Successful'));
}
}}
/>
</MyTooltip>
<MyTooltip label={t('module.Preview Plugin')}>
@@ -141,7 +168,10 @@ const Header = ({ plugin, onClose }: Props) => {
aria-label={'save'}
variant={'base'}
onClick={() => {
setPreviewModules(flowNode2Modules({ nodes, edges }));
const modules = flow2ModulesAndCheck();
if (modules) {
setPreviewModules(modules);
}
}}
/>
</MyTooltip>
@@ -151,7 +181,12 @@ const Header = ({ plugin, onClose }: Props) => {
borderRadius={'lg'}
isLoading={isLoading}
aria-label={'save'}
onClick={onclickSave}
onClick={() => {
const modules = flow2ModulesAndCheck();
if (modules) {
onclickSave(modules);
}
}}
/>
</MyTooltip>
</Flex>

View File

@@ -3,7 +3,7 @@ import ReactFlow, { Background, ReactFlowProvider, useNodesState } from 'reactfl
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import { formatPluginToPreviewModule } from '@fastgpt/global/core/module/utils';
import { plugin2ModuleIO } from '@fastgpt/global/core/module/utils';
import MyModal from '@/components/MyModal';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
@@ -37,7 +37,7 @@ const PreviewPlugin = ({
avatar: plugin.avatar,
name: plugin.name,
intro: plugin.intro,
...formatPluginToPreviewModule(plugin._id, modules)
...plugin2ModuleIO(plugin._id, modules)
}
})
]);

View File

@@ -24,10 +24,12 @@ const Render = ({ pluginId }: Props) => {
const { nodes = [] } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginTemplates } = usePluginStore();
const filterTemplates = useMemo(() => {
const copyTemplates: FlowModuleTemplateType[] = JSON.parse(
JSON.stringify(pluginSystemModuleTemplates)
);
const moduleTemplates = useMemo(() => {
const pluginTemplates = pluginModuleTemplates.filter((item) => item.id !== pluginId);
const concatTemplates = [...pluginSystemModuleTemplates, ...pluginTemplates];
const copyTemplates: FlowModuleTemplateType[] = JSON.parse(JSON.stringify(concatTemplates));
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1,
[FlowNodeTypeEnum.pluginInput]: 1,
@@ -45,8 +47,13 @@ const Render = ({ pluginId }: Props) => {
}
});
// filter hideInPlugin inputs
copyTemplates.forEach((template) => {
template.inputs = template.inputs.filter((input) => !input.hideInPlugin);
});
return copyTemplates;
}, [nodes]);
}, [nodes, pluginId, pluginModuleTemplates]);
const { data: pluginDetail } = useQuery(
['getOnePlugin', pluginId],
@@ -61,17 +68,12 @@ const Render = ({ pluginId }: Props) => {
}
}
);
console.log(pluginDetail);
useQuery(['getPlugTemplates'], () => loadPluginTemplates());
const filterPlugins = useMemo(() => {
return pluginModuleTemplates.filter((item) => item.id !== pluginId);
}, [pluginId, pluginModuleTemplates]);
return pluginDetail ? (
<Flow
systemTemplates={filterTemplates}
pluginTemplates={filterPlugins}
templates={moduleTemplates}
modules={pluginDetail?.modules || []}
Header={<Header plugin={pluginDetail} onClose={() => router.back()} />}
/>

View File

@@ -1,14 +1,5 @@
import React, { useCallback, useState } from 'react';
import {
Box,
Flex,
Button,
ModalHeader,
ModalBody,
Input,
Textarea,
IconButton
} from '@chakra-ui/react';
import { Box, Flex, Button, ModalBody, Input, Textarea, IconButton } from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
@@ -25,6 +16,8 @@ import { useTranslation } from 'next-i18next';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import MyIcon from '@/components/Icon';
import { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
export type FormType = CreateOnePluginParams & {
id?: string;
@@ -35,7 +28,7 @@ export const defaultForm: FormType = {
intro: '',
modules: [
{
moduleId: 'w90mfp',
moduleId: nanoid(),
name: '定义插件输入',
avatar: '/imgs/module/input.png',
flowType: 'pluginInput',
@@ -44,30 +37,11 @@ export const defaultForm: FormType = {
x: 616.4226348688949,
y: -165.05298493910115
},
inputs: [
{
key: 'question',
valueType: 'string',
type: 'target',
label: '用户问题',
required: true,
edit: true,
connected: false
}
],
outputs: [
{
key: 'question',
valueType: 'string',
label: '用户问题',
type: 'source',
edit: true,
targets: []
}
]
inputs: [],
outputs: []
},
{
moduleId: 'tze1ju',
moduleId: nanoid(),
name: '定义插件输出',
avatar: '/imgs/module/output.png',
flowType: 'pluginOutput',
@@ -76,27 +50,8 @@ export const defaultForm: FormType = {
x: 1607.7142331269126,
y: -151.8669210746189
},
inputs: [
{
key: 'answer',
type: 'target',
valueType: 'string',
label: '答案',
required: true,
edit: true,
connected: true
}
],
outputs: [
{
key: 'answer',
valueType: 'string',
label: '答案',
type: 'source',
edit: true,
targets: []
}
]
inputs: [],
outputs: []
}
]
};
@@ -127,7 +82,7 @@ const CreateModal = ({
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png,.svg',
fileType: 'image/*',
multiple: false
});

View File

@@ -40,17 +40,19 @@ export const dispatchClassifyQuestion = async (props: Props): Promise<CQResponse
const cqModel = getCQModel(model);
const chatHistories = getHistories(history, histories);
const { arg, tokens } = await (async () => {
if (cqModel.functionCall) {
return functionCall({
...props,
histories: getHistories(history, histories),
histories: chatHistories,
cqModel
});
}
return completions({
...props,
histories: getHistories(history, histories),
histories: chatHistories,
cqModel
});
})();
@@ -65,7 +67,8 @@ export const dispatchClassifyQuestion = async (props: Props): Promise<CQResponse
query: userChatInput,
tokens,
cqList: agents,
cqResult: result.value
cqResult: result.value,
contextTotalLen: chatHistories.length + 2
}
};
};
@@ -115,7 +118,7 @@ ${systemPrompt}
required: ['type']
}
};
const ai = getAIApi(user.openaiAccount, 48000);
const ai = getAIApi(user.openaiAccount, 480000);
const response = await ai.chat.completions.create({
model: cqModel.model,

View File

@@ -38,18 +38,19 @@ export async function dispatchContentExtract(props: Props): Promise<Response> {
}
const extractModel = global.extractModels[0];
const chatHistories = getHistories(history, histories);
const { arg, tokens } = await (async () => {
if (extractModel.functionCall) {
return functionCall({
...props,
histories: getHistories(history, histories),
histories: chatHistories,
extractModel
});
}
return completions({
...props,
histories: getHistories(history, histories),
histories: chatHistories,
extractModel
});
})();
@@ -84,7 +85,8 @@ export async function dispatchContentExtract(props: Props): Promise<Response> {
query: content,
tokens,
extractDescription: description,
extractResult: arg
extractResult: arg,
contextTotalLen: chatHistories.length + 2
}
};
}
@@ -100,11 +102,11 @@ async function functionCall({
{
obj: ChatRoleEnum.Human,
value: `<任务描述>
${description || '根据用户要求取适当的 JSON 字符串。'}
${description || '根据用户要求取适当的 JSON 字符串。'}
- 如果字段为空,你返回空字符串。
- 不要换行。
- 结合历史记录和文本进行取。
- 结合历史记录和文本进行取。
</任务描述>
<文本>
@@ -128,7 +130,8 @@ ${content}
extractKeys.forEach((item) => {
properties[item.key] = {
type: 'string',
description: item.desc
description: item.desc,
...(item.enum ? { enum: item.enum.split('\n') } : {})
};
});
@@ -192,7 +195,9 @@ async function completions({
json: extractKeys
.map(
(item) =>
`{"key":"${item.key}", "description":"${item.required}", "required":${item.required}}}`
`{"key":"${item.key}", "description":"${item.required}", "required":${item.required}${
item.enum ? `, "enum":"[${item.enum.split('\n')}]"` : ''
}}`
)
.join('\n'),
text: `${histories.map((item) => `${item.obj}:${item.value}`).join('\n')}

View File

@@ -202,7 +202,8 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
query: userChatInput,
maxToken: max_tokens,
quoteList: filterQuoteQA,
historyPreview: getHistoryPreview(completeMessages)
historyPreview: getHistoryPreview(completeMessages),
contextTotalLen: completeMessages.length
},
history: completeMessages
};

View File

@@ -26,6 +26,21 @@ import { dispatchRunPlugin } from './plugin/run';
import { dispatchPluginInput } from './plugin/runInput';
import { dispatchPluginOutput } from './plugin/runOutput';
const callbackMap: Record<string, Function> = {
[FlowNodeTypeEnum.historyNode]: dispatchHistory,
[FlowNodeTypeEnum.questionInput]: dispatchChatInput,
[FlowNodeTypeEnum.answerNode]: dispatchAnswer,
[FlowNodeTypeEnum.chatNode]: dispatchChatCompletion,
[FlowNodeTypeEnum.datasetSearchNode]: dispatchDatasetSearch,
[FlowNodeTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowNodeTypeEnum.contentExtract]: dispatchContentExtract,
[FlowNodeTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowNodeTypeEnum.runApp]: dispatchAppRequest,
[FlowNodeTypeEnum.pluginModule]: dispatchRunPlugin,
[FlowNodeTypeEnum.pluginInput]: dispatchPluginInput,
[FlowNodeTypeEnum.pluginOutput]: dispatchPluginOutput
};
/* running */
export async function dispatchModules({
res,
@@ -111,6 +126,7 @@ export async function dispatchModules({
Object.entries(data).map(([key, val]: any) => {
updateInputValue(key, val);
});
return;
}
function moduleOutput(
@@ -119,6 +135,7 @@ export async function dispatchModules({
): Promise<any> {
pushStore(module, result);
//
const nextRunModules: RunningModuleItemType[] = [];
// Assign the output value to the next module
@@ -141,18 +158,19 @@ export async function dispatchModules({
});
});
return checkModulesCanRun(nextRunModules);
}
function checkModulesCanRun(modules: RunningModuleItemType[] = []) {
// Ensure the uniqueness of running modules
const set = new Set<string>();
const filterModules = modules.filter((module) => {
const filterModules = nextRunModules.filter((module) => {
if (set.has(module.moduleId)) return false;
set.add(module.moduleId);
return true;
});
return checkModulesCanRun(filterModules);
}
function checkModulesCanRun(modules: RunningModuleItemType[] = []) {
return Promise.all(
filterModules.map((module) => {
modules.map((module) => {
if (!module.inputs.find((item: any) => item.value === undefined)) {
moduleInput(module, { [ModuleInputKeyEnum.switch]: undefined });
return moduleRun(module);
@@ -192,20 +210,6 @@ export async function dispatchModules({
};
const dispatchRes: Record<string, any> = await (async () => {
const callbackMap: Record<string, Function> = {
[FlowNodeTypeEnum.historyNode]: dispatchHistory,
[FlowNodeTypeEnum.questionInput]: dispatchChatInput,
[FlowNodeTypeEnum.answerNode]: dispatchAnswer,
[FlowNodeTypeEnum.chatNode]: dispatchChatCompletion,
[FlowNodeTypeEnum.datasetSearchNode]: dispatchDatasetSearch,
[FlowNodeTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowNodeTypeEnum.contentExtract]: dispatchContentExtract,
[FlowNodeTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowNodeTypeEnum.runApp]: dispatchAppRequest,
[FlowNodeTypeEnum.pluginModule]: dispatchRunPlugin,
[FlowNodeTypeEnum.pluginInput]: dispatchPluginInput,
[FlowNodeTypeEnum.pluginOutput]: dispatchPluginOutput
};
if (callbackMap[module.flowType]) {
return callbackMap[module.flowType](props);
}
@@ -232,6 +236,11 @@ export async function dispatchModules({
// start process width initInput
const initModules = runningModules.filter((item) => initRunningModuleType[item.flowType]);
// runningModules.forEach((item) => {
// console.log(item);
// });
initModules.map((module) =>
moduleInput(module, {
...startParams,

View File

@@ -1,7 +1,11 @@
import type { ModuleDispatchProps } from '@/types/core/chat/type';
import { dispatchModules } from '../index';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum, ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import {
DYNAMIC_INPUT_KEY,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import type { moduleDispatchResType } from '@fastgpt/global/core/chat/type.d';
import { getPluginRuntimeById } from '@fastgpt/service/core/plugin/controller';
import { authPluginCanUse } from '@fastgpt/service/support/permission/auth/plugin';
@@ -29,13 +33,37 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
await authPluginCanUse({ id: pluginId, teamId, tmbId });
const plugin = await getPluginRuntimeById(pluginId);
// concat dynamic inputs
const inputModule = plugin.modules.find((item) => item.flowType === FlowNodeTypeEnum.pluginInput);
if (!inputModule) return Promise.reject('Plugin error, It has no set input.');
const hasDynamicInput = inputModule.inputs.find((input) => input.key === DYNAMIC_INPUT_KEY);
const startParams: Record<string, any> = (() => {
if (!hasDynamicInput) return data;
const params: Record<string, any> = {
[DYNAMIC_INPUT_KEY]: {}
};
for (const key in data) {
const input = inputModule.inputs.find((input) => input.key === key);
if (input) {
params[key] = data[key];
} else {
params[DYNAMIC_INPUT_KEY][key] = data[key];
}
}
return params;
})();
const { responseData, answerText } = await dispatchModules({
...props,
modules: plugin.modules.map((module) => ({
...module,
showStatus: false
})),
startParams: data
startParams
});
const output = responseData.find((item) => item.moduleType === FlowNodeTypeEnum.pluginOutput);
@@ -46,6 +74,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
return {
answerText,
// responseData, // debug
responseData: {
moduleLogo: plugin.avatar,
price: responseData.reduce((sum, item) => sum + (item.price || 0), 0),

View File

@@ -1,9 +1,14 @@
import type { moduleDispatchResType } from '@fastgpt/global/core/chat/type.d';
import type { ModuleDispatchProps } from '@/types/core/chat/type';
import { ModuleInputKeyEnum, ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import axios from 'axios';
import { flatDynamicParams } from '../utils';
export type HttpRequestProps = ModuleDispatchProps<{
[ModuleInputKeyEnum.httpUrl]: string;
[ModuleInputKeyEnum.abandon_httpUrl]: string;
[ModuleInputKeyEnum.httpMethod]: string;
[ModuleInputKeyEnum.httpReqUrl]: string;
[ModuleInputKeyEnum.httpHeader]: string;
[key: string]: any;
}>;
export type HttpResponse = {
@@ -12,39 +17,98 @@ export type HttpResponse = {
[key: string]: any;
};
export const dispatchHttpRequest = async (props: Record<string, any>): Promise<HttpResponse> => {
const {
export const dispatchHttpRequest = async (props: HttpRequestProps): Promise<HttpResponse> => {
let {
appId,
chatId,
variables,
inputs: { url, ...body }
} = props as HttpRequestProps;
inputs: {
system_httpMethod: httpMethod,
url: abandonUrl,
system_httpReqUrl: httpReqUrl,
system_httpHeader: httpHeader,
...body
}
} = props;
const requestBody = {
...body,
chatId,
variables
};
body = flatDynamicParams(body);
const { requestMethod, requestUrl, requestHeader, requestBody, requestQuery } = await (() => {
// 2024-2-12 clear
if (abandonUrl) {
return {
requestMethod: 'POST',
requestUrl: abandonUrl,
requestHeader: httpHeader,
requestBody: {
...body,
appId,
chatId,
variables
},
requestQuery: {}
};
}
if (httpReqUrl) {
return {
requestMethod: httpMethod,
requestUrl: httpReqUrl,
requestHeader: httpHeader,
requestBody: {
appId,
chatId,
variables,
data: body
},
requestQuery: {
appId,
chatId,
...variables,
...body
}
};
}
return Promise.reject('url is empty');
})();
const formatBody = transformFlatJson({ ...requestBody });
// parse header
const headers = await (() => {
try {
if (!requestHeader) return {};
return JSON.parse(requestHeader);
} catch (error) {
return Promise.reject('Header 为非法 JSON 格式');
}
})();
try {
const response = await fetchData({
url,
body: requestBody
method: requestMethod,
url: requestUrl,
headers,
body: formatBody,
query: requestQuery
});
return {
responseData: {
price: 0,
body: requestBody,
body: formatBody,
httpResult: response
},
...response
};
} catch (error) {
console.log(error);
return {
[ModuleOutputKeyEnum.failed]: true,
responseData: {
price: 0,
body: requestBody,
body: formatBody,
httpResult: { error }
}
};
@@ -52,19 +116,97 @@ export const dispatchHttpRequest = async (props: Record<string, any>): Promise<H
};
async function fetchData({
method,
url,
body
headers,
body,
query
}: {
method: string;
url: string;
headers: Record<string, any>;
body: Record<string, any>;
query: Record<string, any>;
}): Promise<Record<string, any>> {
const response = await fetch(url, {
method: 'POST',
const { data: response } = await axios<Record<string, any>>({
method,
baseURL: `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`,
url,
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body)
}).then((res) => res.json());
params: method === 'GET' ? query : {},
data: method === 'POST' ? body : {}
});
return response;
/*
parse the json:
{
user: {
name: 'xxx',
age: 12
}
psw: 'xxx'
}
result: {
'user': {
name: 'xxx',
age: 12
},
'user.name': 'xxx',
'user.age': 12,
'psw': 'xxx'
}
*/
const parseJson = (obj: Record<string, any>, prefix = '') => {
let result: Record<string, any> = {};
for (const key in obj) {
if (typeof obj[key] === 'object') {
result[key] = obj[key];
result = {
...result,
...parseJson(obj[key], `${prefix}${key}.`)
};
} else {
result[`${prefix}${key}`] = obj[key];
}
}
return result;
};
return parseJson(response);
}
function transformFlatJson(obj: Record<string, any>) {
for (let key in obj) {
if (typeof obj[key] === 'object') {
transformFlatJson(obj[key]);
}
if (key.includes('.')) {
let parts = key.split('.');
if (parts.length <= 1) continue;
const firstKey = parts.shift();
if (!firstKey) continue;
const lastKey = parts.join('.');
if (obj[firstKey]) {
obj[firstKey] = {
...obj[firstKey],
[lastKey]: obj[key]
};
} else {
obj[firstKey] = { [lastKey]: obj[key] };
}
transformFlatJson(obj[firstKey]);
delete obj[key];
}
}
return obj;
}

View File

@@ -8,6 +8,7 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { sseResponseEventEnum } from '@fastgpt/service/common/response/constant';
import { textAdaptGptResponse } from '@/utils/adapt';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import { getHistories } from '../utils';
type Props = ModuleDispatchProps<{
userChatInput: string;
@@ -26,6 +27,7 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
user,
stream,
detail,
histories,
inputs: { userChatInput, history = [], app }
} = props;
@@ -52,17 +54,19 @@ export const dispatchAppRequest = async (props: Props): Promise<Response> => {
});
}
const chatHistories = getHistories(history, histories);
const { responseData, answerText } = await dispatchModules({
...props,
appId: app.id,
modules: appData.modules,
histories: history,
histories: chatHistories,
startParams: {
userChatInput
}
});
const completeMessages = history.concat([
const completeMessages = chatHistories.concat([
{
obj: ChatRoleEnum.Human,
value: userChatInput

View File

@@ -1,4 +1,5 @@
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
import { DYNAMIC_INPUT_KEY } from '@fastgpt/global/core/module/constants';
export const getHistories = (history?: ChatItemType[] | number, histories: ChatItemType[] = []) => {
if (!history) return [];
@@ -7,3 +8,13 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
return [];
};
export const flatDynamicParams = (params: Record<string, any>) => {
const dynamicParams = params[DYNAMIC_INPUT_KEY];
if (!dynamicParams) return params;
return {
...params,
...dynamicParams,
[DYNAMIC_INPUT_KEY]: undefined
};
};

View File

@@ -6,6 +6,7 @@ import { addLog } from '@fastgpt/service/common/system/log';
import type { ConcatBillProps, CreateBillProps } from '@fastgpt/global/support/wallet/bill/api.d';
import { defaultQGModels } from '@fastgpt/global/core/ai/model';
import { POST } from '@fastgpt/service/common/api/plusRequest';
import { PostReRankProps } from '@fastgpt/global/core/ai/api';
export function createBill(data: CreateBillProps) {
if (!global.systemEnv?.pluginBaseUrl) return;
@@ -247,16 +248,21 @@ export function pushWhisperBill({
export function pushReRankBill({
teamId,
tmbId,
source
source,
inputs
}: {
teamId: string;
tmbId: string;
source: `${BillSourceEnum}`;
inputs: PostReRankProps['inputs'];
}) {
const model = global.reRankModels[0];
if (!model) return { total: 0 };
const total = model.price * PRICE_SCALE;
const textLength = inputs.reduce((sum, item) => sum + item.text.length, 0);
const ratio = Math.ceil(textLength / 1000);
const total = model.price * PRICE_SCALE * ratio;
const name = 'wallet.bill.ReRank';
createBill({

View File

@@ -1,5 +1,5 @@
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { XYPosition } from 'reactflow';
import { AppModuleItemTypeEnum, ModulesInputItemTypeEnum } from '../constants/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';

View File

@@ -2,11 +2,11 @@ import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
import type { ChatMessageItemType } from '@fastgpt/global/core/ai/type.d';
import type { ModuleItemType, FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import type { Edge, Node } from 'reactflow';
import { connectionLineStyle } from '@/web/core/modules/constants/flowUi';
import { customAlphabet } from 'nanoid';
import { EmptyModule } from '@fastgpt/global/core/module/template/system/empty';
import { moduleTemplatesFlat } from '@/web/core/modules/template/system';
import { adaptRole_Message2Chat } from '@fastgpt/global/core/chat/adapt';
import { EDGE_TYPE } from '@fastgpt/global/core/module/node/constant';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
export const gptMessage2ChatType = (messages: ChatMessageItemType[]): ChatItemType[] => {
@@ -100,14 +100,12 @@ export const appModule2FlowEdge = ({
module.outputs.forEach((output) =>
output.targets.forEach((target) => {
edges.push({
style: connectionLineStyle,
source: module.moduleId,
target: target.moduleId,
sourceHandle: output.key,
targetHandle: target.key,
id: nanoid(),
animated: true,
type: 'buttonedge',
type: EDGE_TYPE,
data: { onDelete }
});
})

View File

@@ -9,7 +9,7 @@ export const postUploadFiles = (
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
) =>
POST<string[]>('/common/file/upload', data, {
timeout: 0,
timeout: 480000,
onUploadProgress,
headers: {
'Content-Type': 'multipart/form-data; charset=utf-8'

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDisclosure, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyModal from '@/components/MyModal';
@@ -7,7 +7,6 @@ export const useConfirm = (props?: {
title?: string;
iconSrc?: string | '';
content?: string;
bg?: string;
showCancel?: boolean;
type?: 'common' | 'delete';
}) => {
@@ -34,7 +33,6 @@ export const useConfirm = (props?: {
title = map?.title || t('Warning'),
iconSrc = map?.iconSrc,
content,
bg = map?.bg,
showCancel = true
} = props || {};
const [customContent, setCustomContent] = useState(content);
@@ -57,43 +55,72 @@ export const useConfirm = (props?: {
[onOpen]
),
ConfirmModal: useCallback(
({ isLoading }: { isLoading?: boolean }) => (
<MyModal
isOpen={isOpen}
onClose={onClose}
iconSrc={iconSrc}
title={title}
maxW={['90vw', '500px']}
>
<ModalBody pt={5}>{customContent}</ModalBody>
<ModalFooter>
{showCancel && (
({
closeText = t('Cancel'),
confirmText = t('Confirm'),
isLoading,
bg,
countDown = 0
}: {
closeText?: string;
confirmText?: string;
isLoading?: boolean;
bg?: string;
countDown?: number;
}) => {
const timer = useRef<any>();
const [countDownAmount, setCountDownAmount] = useState(countDown);
useEffect(() => {
timer.current = setInterval(() => {
setCountDownAmount((val) => {
if (val <= 0) {
clearInterval(timer.current);
}
return val - 1;
});
}, 1000);
}, []);
return (
<MyModal
isOpen={isOpen}
onClose={onClose}
iconSrc={iconSrc}
title={title}
maxW={['90vw', '500px']}
>
<ModalBody pt={5}>{customContent}</ModalBody>
<ModalFooter>
{showCancel && (
<Button
variant={'base'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
{closeText}
</Button>
)}
<Button
variant={'base'}
{...(bg && { bg: `${bg} !important` })}
isDisabled={countDownAmount > 0}
ml={4}
isLoading={isLoading}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
{t('Cancel')}
{countDownAmount > 0 ? `${countDownAmount}s` : confirmText}
</Button>
)}
<Button
{...(bg && { bg: `${bg} !important` })}
ml={4}
isLoading={isLoading}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
{t('Confirm')}
</Button>
</ModalFooter>
</MyModal>
),
[bg, customContent, iconSrc, isOpen, onClose, showCancel, t, title]
</ModalFooter>
</MyModal>
);
},
[customContent, iconSrc, isOpen, onClose, showCancel, t, title]
)
};
};

View File

@@ -21,6 +21,8 @@ export const useCopyData = () => {
throw new Error('');
}
} catch (error) {
console.log(error);
const textarea = document.createElement('textarea');
textarea.value = data;
document.body.appendChild(textarea);

View File

@@ -2,4 +2,4 @@ import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
import { UrlFetchParams, UrlFetchResponse } from '@fastgpt/global/common/file/api.d';
export const postFetchUrls = (data: UrlFetchParams) =>
POST<UrlFetchResponse>(`/tools/urlFetch`, data);
POST<UrlFetchResponse>(`/common/tools/urlFetch`, data);

View File

@@ -1,60 +1,48 @@
import type { BoxProps } from '@chakra-ui/react';
import { ModuleDataTypeEnum } from '@fastgpt/global/core/module/constants';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
export const FlowValueTypeStyle: Record<`${ModuleDataTypeEnum}`, BoxProps> = {
[ModuleDataTypeEnum.string]: {
background: '#36ADEF'
},
[ModuleDataTypeEnum.number]: {
background: '#FB7C3C'
},
[ModuleDataTypeEnum.boolean]: {
background: '#E7D118'
},
[ModuleDataTypeEnum.chatHistory]: {
background: '#00A9A6'
},
[ModuleDataTypeEnum.datasetQuote]: {
background: '#A558C9'
},
[ModuleDataTypeEnum.any]: {
background: '#9CA2A8'
},
[ModuleDataTypeEnum.selectApp]: {
background: '#6a6efa'
},
[ModuleDataTypeEnum.selectDataset]: {
background: '#21ba45'
}
};
export const FlowValueTypeMap = {
[ModuleDataTypeEnum.string]: {
[ModuleIOValueTypeEnum.string]: {
handlerStyle: {
background: '#36ADEF'
},
label: 'core.module.valueType.string',
value: ModuleDataTypeEnum.string,
example: ''
value: ModuleIOValueTypeEnum.string,
description: ''
},
[ModuleDataTypeEnum.number]: {
[ModuleIOValueTypeEnum.number]: {
handlerStyle: {
background: '#FB7C3C'
},
label: 'core.module.valueType.number',
value: ModuleDataTypeEnum.number,
example: ''
value: ModuleIOValueTypeEnum.number,
description: ''
},
[ModuleDataTypeEnum.boolean]: {
[ModuleIOValueTypeEnum.boolean]: {
handlerStyle: {
background: '#E7D118'
},
label: 'core.module.valueType.boolean',
value: ModuleDataTypeEnum.boolean,
example: ''
value: ModuleIOValueTypeEnum.boolean,
description: ''
},
[ModuleDataTypeEnum.chatHistory]: {
[ModuleIOValueTypeEnum.chatHistory]: {
handlerStyle: {
background: '#00A9A6'
},
label: 'core.module.valueType.chatHistory',
value: ModuleDataTypeEnum.chatHistory,
example: `{
value: ModuleIOValueTypeEnum.chatHistory,
description: `{
obj: System | Human | AI;
value: string;
}[]`
},
[ModuleDataTypeEnum.datasetQuote]: {
[ModuleIOValueTypeEnum.datasetQuote]: {
handlerStyle: {
background: '#A558C9'
},
label: 'core.module.valueType.datasetQuote',
value: ModuleDataTypeEnum.datasetQuote,
example: `{
value: ModuleIOValueTypeEnum.datasetQuote,
description: `{
id: string;
datasetId: string;
collectionId: string;
@@ -64,19 +52,28 @@ export const FlowValueTypeMap = {
a: string
}[]`
},
[ModuleDataTypeEnum.any]: {
[ModuleIOValueTypeEnum.any]: {
handlerStyle: {
background: '#9CA2A8'
},
label: 'core.module.valueType.any',
value: ModuleDataTypeEnum.any,
example: ''
value: ModuleIOValueTypeEnum.any,
description: ''
},
[ModuleDataTypeEnum.selectApp]: {
[ModuleIOValueTypeEnum.selectApp]: {
handlerStyle: {
background: '#6a6efa'
},
label: 'core.module.valueType.selectApp',
value: ModuleDataTypeEnum.selectApp,
example: ''
value: ModuleIOValueTypeEnum.selectApp,
description: ''
},
[ModuleDataTypeEnum.selectDataset]: {
[ModuleIOValueTypeEnum.selectDataset]: {
handlerStyle: {
background: '#21ba45'
},
label: 'core.module.valueType.selectDataset',
value: ModuleDataTypeEnum.selectDataset,
example: ''
value: ModuleIOValueTypeEnum.selectDataset,
description: ''
}
};

View File

@@ -1,7 +0,0 @@
export const edgeOptions = {
style: {
strokeWidth: 1.5,
stroke: '#5A646Es'
}
};
export const connectionLineStyle = { strokeWidth: 1.5, stroke: '#5A646Es' };

View File

@@ -1,6 +1,6 @@
import { UserGuideModule } from '@fastgpt/global/core/module/template/system/userGuide';
import { UserInputModule } from '@fastgpt/global/core/module/template/system/userInput';
import { HistoryModule } from '@fastgpt/global/core/module/template/system/history';
import { HistoryModule } from '@fastgpt/global/core/module/template/system/abandon/history';
import { AiChatModule } from '@fastgpt/global/core/module/template/system/aiChat';
import { DatasetSearchModule } from '@fastgpt/global/core/module/template/system/datasetSearch';
import { AssignedAnswerModule } from '@fastgpt/global/core/module/template/system/assignedAnswer';
@@ -12,6 +12,7 @@ import { RunAppModule } from '@fastgpt/global/core/module/template/system/runApp
import { PluginInputModule } from '@fastgpt/global/core/module/template/system/pluginInput';
import { PluginOutputModule } from '@fastgpt/global/core/module/template/system/pluginOutput';
import { RunPluginModule } from '@fastgpt/global/core/module/template/system/runPlugin';
import type {
FlowModuleTemplateType,
moduleTemplateListType
@@ -68,19 +69,19 @@ export const moduleTemplatesList: moduleTemplateListType = [
label: '系统输入',
list: []
},
{
type: ModuleTemplateTypeEnum.tools,
label: '工具',
list: []
},
{
type: ModuleTemplateTypeEnum.textAnswer,
label: '文本输出',
list: []
},
{
type: ModuleTemplateTypeEnum.dataset,
label: '知识库',
list: []
},
{
type: ModuleTemplateTypeEnum.functionCall,
label: '函数调用',
label: '功能调用',
list: []
},
{
@@ -93,59 +94,9 @@ export const moduleTemplatesList: moduleTemplateListType = [
label: '个人插件',
list: []
},
{
type: ModuleTemplateTypeEnum.communityPlugin,
label: '社区插件',
list: []
},
{
type: ModuleTemplateTypeEnum.commercialPlugin,
label: '商业插件',
list: []
},
{
type: ModuleTemplateTypeEnum.other,
label: '其他',
list: []
}
];
// export const appSystemModuleTemplates = [
// {
// label: '引导模块',
// list: [UserGuideModule]
// },
// {
// label: '输入模块',
// list: [UserInputModule, HistoryModule]
// },
// {
// label: '内容生成',
// list: [AiChatModule, AssignedAnswerModule]
// },
// {
// label: '核心调用',
// list: [DatasetSearchModule, RunAppModule]
// },
// {
// label: '函数模块',
// list: [ClassifyQuestionModule, ContextExtractModule, HttpModule]
// }
// ];
// export const pluginModuleTemplates = [
// {
// label: '输入输出',
// list: [PluginInputModule, PluginOutputModule, HistoryModule]
// },
// {
// label: '内容生成',
// list: [AiChatModule, AssignedAnswerModule]
// },
// {
// label: '核心调用',
// list: [DatasetSearchModule, RunAppModule]
// },
// {
// label: '函数模块',
// list: [ClassifyQuestionModule, ContextExtractModule, HttpModule]
// }
// ];

View File

@@ -71,7 +71,7 @@ const Button = defineStyleConfig({
color: 'myBlue.700',
border: '1px solid #EFF0F1',
_hover: {
bg: '#3370FF1A'
background: '#3370FF1A'
}
},
base: {
@@ -82,13 +82,16 @@ const Button = defineStyleConfig({
transition: 'background 0.3s',
_hover: {
color: 'myBlue.600',
bg: 'myWhite.400',
background: 'myWhite.400',
boxShadow: '0 0 5px rgba(0,0,0,0.1)'
},
_active: {
color: 'myBlue.700'
},
_disabled: { bg: 'myGray.100 !important', color: 'myGray.700 !important' }
_disabled: {
bg: 'myGray.100 !important',
color: 'myGray.700 !important'
}
},
boxBtn: {
px: 3,