feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077)

* fix: plugin output check

* fix: ui refresh error when continuous file upload

* feat: View will move when workflow check error
This commit is contained in:
Archer 2024-11-05 23:54:10 +08:00 committed by archer
parent 0f1932aadc
commit a9ee6e6a5e
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
17 changed files with 129 additions and 125 deletions

View File

@ -12,9 +12,10 @@ weight: 811
1.
2. 新增 - 文件上传方案调整,节点直接支持接收文件链接,插件自定义变量支持文件上传。
3. 新增 - 对话记录增加时间显示。
4. 优化 - 知识库上传文件,优化报错提示。
5. 优化 - 全文检索语句,减少一轮查询。
6. 优化 - 修改 findLast 为 [...array].reverse().find适配旧版浏览器。
7. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。
8. 修复 - Dockerfile pnpm install 支持代理。
9. 修复 - BI 图表生成无法写入文件。
4. 新增 - 工作流校验错误时,跳转至错误节点。
5. 优化 - 知识库上传文件,优化报错提示。
6. 优化 - 全文检索语句,减少一轮查询。
7. 优化 - 修改 findLast 为 [...array].reverse().find适配旧版浏览器。
8. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。
9. 修复 - Dockerfile pnpm install 支持代理。
10. 修复 - BI 图表生成无法写入文件。

View File

@ -500,7 +500,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
nodes: runtimeNodes,
variables
});
console.log(value, '=-=-');
// Dynamic input is stored in the dynamic key
if (input.canEdit && dynamicInput && params[dynamicInput.key]) {
params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType);

View File

@ -43,7 +43,7 @@ export const dispatchLoop = async (props: Props): Promise<Response> => {
let totalPoints = 0;
let newVariables: Record<string, any> = props.variables;
for await (const item of loopInputArray) {
for await (const item of loopInputArray.filter(Boolean)) {
runtimeNodes.forEach((node) => {
if (
childrenNodeIdList.includes(node.nodeId) &&

View File

@ -196,16 +196,13 @@ const VariableEdit = ({
<Thead h={8}>
<Tr>
<Th
fontSize={'mini'}
borderRadius={'none !important'}
fontSize={'mini'}
bg={'myGray.50'}
p={0}
px={4}
fontWeight={'medium'}
>
{t('common:core.module.variable.key')}
</Th>
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
{t('workflow:Variable_name')}
</Th>
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
@ -236,19 +233,9 @@ const VariableEdit = ({
>
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.label}
{item.key}
</Flex>
</Td>
<Td
p={0}
px={4}
h={8}
color={'myGray.900'}
fontSize={'mini'}
fontWeight={'medium'}
>
<Flex alignItems={'center'}>{item.key}</Flex>
</Td>
<Td p={0} px={4} h={8} color={'myGray.900'} fontSize={'mini'}>
<Flex alignItems={'center'}>
{item.required ? (

View File

@ -73,7 +73,8 @@ const ChatInput = ({
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
@ -81,7 +82,6 @@ const ChatInput = ({
fileCtrl
});
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
const canSendMessage = havInput && !hasFileUploading;
// Upload files
@ -206,7 +206,7 @@ const ChatInput = ({
<MyTooltip label={selectFileLabel}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip>
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</Flex>
)}
@ -282,7 +282,7 @@ const ChatInput = ({
.filter((file) => {
return file && fileTypeFilter(file);
}) as File[];
onSelectFile({ files, fileList });
onSelectFile({ files });
if (files.length > 0) {
e.stopPropagation();

View File

@ -33,8 +33,10 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
update: updateFiles,
remove: removeFiles,
fields: fileList,
replace: replaceFiles
replace: replaceFiles,
append: appendFiles
} = fileCtrl;
const hasFileUploading = fileList.some((item) => !item.url);
const showSelectFile = fileSelectConfig?.canSelectFile;
const showSelectImg = fileSelectConfig?.canSelectImg;
@ -69,7 +71,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
});
const onSelectFile = useCallback(
async ({ files, fileList }: { files: File[]; fileList: UserInputFileItemType[] }) => {
async ({ files }: { files: File[] }) => {
if (!files || files.length === 0) {
return [];
}
@ -128,22 +130,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
)
);
// Document, image
const concatFileList = clone(
fileList.concat(loadFiles).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
})
);
replaceFiles(concatFileList);
appendFiles(loadFiles);
return loadFiles;
},
[maxSelectFiles, replaceFiles, toast, t, maxSize]
[maxSelectFiles, appendFiles, toast, t, maxSize]
);
const uploadFiles = async () => {
@ -197,10 +188,23 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
removeFiles(errorFileIndex);
};
const sortFileList = useMemo(() => {
// Sort: Document, image
const sortResult = clone(fileList).sort((a, b) => {
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
return 1;
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
return -1;
}
return 0;
});
return sortResult;
}, [fileList]);
return {
File,
onOpenSelectFile,
fileList,
fileList: sortFileList,
onSelectFile,
uploadFiles,
selectFileIcon,
@ -208,6 +212,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
};
};

View File

@ -56,7 +56,7 @@ const RenderInput = () => {
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
@ -64,9 +64,7 @@ const RenderInput = () => {
fileCtrl
});
const isDisabledInput = histories.length > 0;
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
@ -83,6 +81,7 @@ const RenderInput = () => {
[onNewChat, setRestartData]
);
// Get plugin input components
const formatPluginInputs = useMemo(() => {
if (histories.length === 0) return pluginInputs;
try {
@ -203,7 +202,7 @@ const RenderInput = () => {
{t('chat:select')}
</Button>
)}
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</Flex>
<FilePreview
fileList={fileList}

View File

@ -50,7 +50,8 @@ const FileSelector = ({
onOpenSelectFile,
onSelectFile,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
@ -84,9 +85,6 @@ const FileSelector = ({
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useEffect(() => {
setUploading(hasFileUploading);
@ -128,7 +126,7 @@ const FileSelector = ({
<FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} />
{fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />}
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</>
);
};

View File

@ -13,13 +13,16 @@ import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next';
import { ReactFlowProvider } from 'reactflow';
import { useTranslation } from 'next-i18next';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
@ -64,9 +67,11 @@ const WorkflowEdit = () => {
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
<ReactFlowProvider>
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowProvider>
);
};

View File

@ -11,15 +11,18 @@ import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import { useTranslation } from 'next-i18next';
import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next';
import { ReactFlowProvider } from 'reactflow';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
@ -64,9 +67,11 @@ const WorkflowEdit = () => {
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
<ReactFlowProvider>
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowProvider>
);
};

View File

@ -475,8 +475,7 @@ const RenderList = React.memo(function RenderList({
NodeOutputKeyEnum.userChatInput
];
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
node.nodeId,
NodeOutputKeyEnum.userFiles
[node.nodeId, NodeOutputKeyEnum.userFiles]
];
}
});

View File

@ -169,12 +169,4 @@ const Workflow = () => {
);
};
const Render = () => {
return (
<ReactFlowProvider>
<Workflow />
</ReactFlowProvider>
);
};
export default React.memo(Render);
export default React.memo(Workflow);

View File

@ -172,6 +172,7 @@ const InputTypeConfig = ({
</FormLabel>
<Input
bg={'myGray.50'}
maxLength={30}
placeholder="appointment/sql"
{...register('label', {
required: true

View File

@ -247,11 +247,18 @@ const MultipleReferenceSelector = ({
const ArraySelector = useMemo(() => {
const selectorVal = value as ReferenceItemValueType[];
const notValidItem =
!selectorVal ||
selectorVal.length === 0 ||
selectorVal.every((item) => {
const [nodeName, outputName] = getSelectValue(item);
return !nodeName || !outputName;
});
return (
<MultipleRowArraySelect
label={
selectorVal && selectorVal.length > 0 ? (
!notValidItem ? (
<Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}>
{selectorVal.map((item, index) => {
const [nodeName, outputName] = getSelectValue(item);
@ -261,36 +268,35 @@ const MultipleReferenceSelector = ({
<Flex
alignItems={'center'}
key={index}
bg={isInvalidItem ? 'red.50' : 'primary.50'}
color={isInvalidItem ? 'red.600' : 'myGray.900'}
bg={'primary.50'}
color={'myGray.900'}
py={1}
px={1.5}
rounded={'sm'}
>
<Flex alignItems={'center'} flex={1}>
{isInvalidItem ? (
t('common:invalid_variable')
) : (
<>
{nodeName}
<MyIcon
name={'common/rightArrowLight'}
mx={1}
w={'12px'}
color={'myGray.500'}
/>
{outputName}
</>
)}
<Flex
alignItems={'center'}
flex={'1 0 0'}
maxW={'200px'}
className="textEllipsis"
>
{nodeName}
<MyIcon
name={'common/rightArrowLight'}
mx={1}
w={'12px'}
color={'myGray.500'}
/>
{outputName}
</Flex>
<MyIcon
name={'common/closeLight'}
w={'16px'}
w={'1rem'}
ml={1}
cursor={'pointer'}
color={'myGray.500'}
_hover={{
color: 'primary.600'
color: 'red.600'
}}
onClick={(e) => {
e.stopPropagation();

View File

@ -32,7 +32,8 @@ import {
NodeChange,
OnConnectStartParams,
useEdgesState,
useNodesState
useNodesState,
useReactFlow
} from 'reactflow';
import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants';
@ -568,6 +569,7 @@ const WorkflowContextProvider = ({
);
/* ui flow to store data */
const { fitView } = useReactFlow();
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
@ -577,6 +579,12 @@ const WorkflowContextProvider = ({
return storeWorkflow;
} else if (!hideTip) {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
// View move to the node that failed
fitView({
nodes: nodes.filter((node) => checkResults.includes(node.data.nodeId))
});
toast({
status: 'warning',
title: t('common:core.workflow.Check Failed')

View File

@ -1,51 +1,47 @@
import React, { useRef, useCallback } from 'react';
import { Box } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
import { useMemoizedFn } from 'ahooks';
export const useSelectFile = (props?: {
fileType?: string;
multiple?: boolean;
maxCount?: number;
}) => {
const { t } = useTranslation();
const { fileT } = useI18n();
const { fileType = '*', multiple = false, maxCount = 10 } = props || {};
const { toast } = useToast();
const SelectFileDom = useRef<HTMLInputElement>(null);
const openSign = useRef<any>();
const File = useCallback(
({ onSelect }: { onSelect: (e: File[], sign?: any) => void }) => (
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input
ref={SelectFileDom}
type="file"
accept={fileType}
multiple={multiple}
onChange={(e) => {
const files = e.target.files;
const File = useMemoizedFn(({ onSelect }: { onSelect: (e: File[], sign?: any) => void }) => (
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input
ref={SelectFileDom}
type="file"
accept={fileType}
multiple={multiple}
onChange={(e) => {
const files = e.target.files;
if (!files || files?.length === 0) return;
if (!files || files?.length === 0) return;
let fileList = Array.from(files);
if (fileList.length > maxCount) {
toast({
status: 'warning',
title: fileT('select_file_amount_limit', { max: maxCount })
});
fileList = fileList.slice(0, maxCount);
}
onSelect(fileList, openSign.current);
let fileList = Array.from(files);
if (fileList.length > maxCount) {
toast({
status: 'warning',
title: fileT('select_file_amount_limit', { max: maxCount })
});
fileList = fileList.slice(0, maxCount);
}
onSelect(fileList, openSign.current);
e.target.value = '';
}}
/>
</Box>
),
[fileT, fileType, maxCount, multiple, toast]
);
e.target.value = '';
}}
/>
</Box>
));
const onOpen = useCallback((sign?: any) => {
openSign.current = sign;

View File

@ -23,7 +23,8 @@ import {
formatEditorVariablePickerIcon,
getAppChatConfig,
getGuideModule,
isReferenceValue
isReferenceValue,
isReferenceValueArray
} from '@fastgpt/global/core/workflow/utils';
import { TFunction } from 'next-i18next';
import {
@ -328,7 +329,8 @@ export const checkWorkflowNodeAndConnection = ({
if (
node.data.flowNodeType === FlowNodeTypeEnum.pluginOutput &&
!(isReferenceValue(input.value, nodeIds) && input.value[1])
(input.value?.length === 0 ||
(isReferenceValue(input.value, nodeIds) && !input.value?.[1]))
) {
return true;
}