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:
parent
0f1932aadc
commit
a9ee6e6a5e
@ -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 图表生成无法写入文件。
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) &&
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 })} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -475,8 +475,7 @@ const RenderList = React.memo(function RenderList({
|
||||
NodeOutputKeyEnum.userChatInput
|
||||
];
|
||||
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
|
||||
node.nodeId,
|
||||
NodeOutputKeyEnum.userFiles
|
||||
[node.nodeId, NodeOutputKeyEnum.userFiles]
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@ -169,12 +169,4 @@ const Workflow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Render = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Workflow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
export default React.memo(Workflow);
|
||||
|
||||
@ -172,6 +172,7 @@ const InputTypeConfig = ({
|
||||
</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
maxLength={30}
|
||||
placeholder="appointment/sql"
|
||||
{...register('label', {
|
||||
required: true
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user