From 1a1bfd40483018cd57319f416c6f47d864ef607e Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 24 Jun 2025 09:10:30 +0800 Subject: [PATCH] feat: last run frontend (#21369) The frontend of feat: Persist Variables for Enhanced Debugging Workflow (#20699). Co-authored-by: jZonG --- web/app/components/app-sidebar/app-info.tsx | 83 ++-- .../app-sidebar/app-sidebar-dropdown.tsx | 125 +++++ web/app/components/app-sidebar/index.tsx | 25 +- .../file-uploader-in-attachment/index.tsx | 16 +- .../components/base/file-uploader/utils.ts | 2 +- web/app/components/base/tab-header/index.tsx | 10 +- web/app/components/header/header-wrapper.tsx | 15 + .../hooks/use-fetch-workflow-inspect-vars.ts | 68 +++ .../workflow-app/hooks/use-workflow-init.ts | 1 - .../workflow-app/hooks/use-workflow-run.ts | 34 +- web/app/components/workflow/constants.ts | 2 + .../workflow/header/header-in-restoring.tsx | 12 +- web/app/components/workflow/header/index.tsx | 7 +- .../workflow/header/run-and-history.tsx | 14 +- .../workflow/hooks/use-inspect-vars-crud.ts | 241 ++++++++++ .../use-nodes-interactions-without-sync.ts | 35 ++ .../workflow/hooks/use-nodes-interactions.ts | 10 +- .../workflow/hooks/use-shortcuts.ts | 12 + .../hooks/use-workflow-interactions.ts | 26 ++ .../components/workflow/hooks/use-workflow.ts | 7 +- web/app/components/workflow/index.tsx | 40 ++ .../components/before-run-form/form-item.tsx | 13 +- .../_base/components/before-run-form/form.tsx | 6 +- .../components/before-run-form/index.tsx | 141 +++--- .../components/before-run-form/panel-wrap.tsx | 41 ++ .../nodes/_base/components/node-control.tsx | 21 +- .../panel-operator/panel-operator-popup.tsx | 6 +- .../_base/components/workflow-panel/index.tsx | 429 ++++++++++++++++++ .../workflow-panel/last-run/index.tsx | 126 +++++ .../workflow-panel/last-run/no-data.tsx | 36 ++ .../workflow-panel/last-run/use-last-run.ts | 330 ++++++++++++++ .../_base/components/workflow-panel/tab.tsx | 34 ++ .../nodes/_base/hooks/use-one-step-run.ts | 171 +++++-- .../nodes/_base/hooks/use-output-var-list.ts | 53 ++- .../components/workflow/nodes/_base/node.tsx | 12 +- .../components/workflow/nodes/_base/panel.tsx | 214 --------- .../components/workflow/nodes/agent/panel.tsx | 54 +-- .../workflow/nodes/agent/use-config.ts | 42 -- .../nodes/agent/use-single-run-form-params.ts | 90 ++++ .../assigner/components/var-list/index.tsx | 1 + .../workflow/nodes/assigner/types.ts | 2 + .../workflow/nodes/assigner/use-config.ts | 2 +- .../assigner/use-single-run-form-params.ts | 55 +++ .../components/workflow/nodes/code/panel.tsx | 31 -- .../workflow/nodes/code/use-config.ts | 46 +- .../nodes/code/use-single-run-form-params.ts | 65 +++ .../nodes/document-extractor/panel.tsx | 37 +- .../nodes/document-extractor/use-config.ts | 46 +- .../use-single-run-form-params.ts | 64 +++ .../components/workflow/nodes/http/panel.tsx | 30 -- .../workflow/nodes/http/use-config.ts | 62 +-- .../nodes/http/use-single-run-form-params.ts | 74 +++ .../condition-list/condition-operator.tsx | 2 +- .../if-else/use-single-run-form-params.ts | 166 +++++++ web/app/components/workflow/nodes/index.tsx | 4 +- .../workflow/nodes/iteration/panel.tsx | 55 +-- .../workflow/nodes/iteration/types.ts | 1 + .../workflow/nodes/iteration/use-config.ts | 171 +------ .../iteration/use-single-run-form-params.ts | 154 +++++++ .../nodes/knowledge-retrieval/panel.tsx | 34 +- .../nodes/knowledge-retrieval/use-config.ts | 41 +- .../use-single-run-form-params.ts | 63 +++ .../json-schema-config-modal/code-editor.tsx | 61 +-- .../schema-editor.tsx | 11 +- .../components/workflow/nodes/llm/default.ts | 25 + .../components/workflow/nodes/llm/panel.tsx | 83 +--- .../workflow/nodes/llm/use-config.ts | 99 +--- .../nodes/llm/use-single-run-form-params.ts | 198 ++++++++ .../components/workflow/nodes/loop/panel.tsx | 44 +- .../workflow/nodes/loop/use-config.ts | 152 +------ .../nodes/loop/use-single-run-form-params.ts | 221 +++++++++ .../nodes/parameter-extractor/panel.tsx | 67 +-- .../nodes/parameter-extractor/use-config.ts | 86 +--- .../use-single-run-form-params.ts | 148 ++++++ .../nodes/question-classifier/panel.tsx | 67 +-- .../nodes/question-classifier/use-config.ts | 71 +-- .../use-single-run-form-params.ts | 146 ++++++ .../workflow/nodes/start/use-config.ts | 20 +- .../nodes/start/use-single-run-form-params.ts | 87 ++++ .../nodes/template-transform/panel.tsx | 29 -- .../nodes/template-transform/use-config.ts | 44 +- .../use-single-run-form-params.ts | 65 +++ .../components/workflow/nodes/tool/panel.tsx | 36 +- .../workflow/nodes/tool/use-config.ts | 97 +--- .../nodes/tool/use-get-data-for-check-more.ts | 20 + .../nodes/tool/use-single-run-form-params.ts | 94 ++++ .../nodes/variable-assigner/use-config.ts | 34 +- .../use-single-run-form-params.ts | 92 ++++ .../workflow/operator/add-block.tsx | 2 +- .../components/workflow/operator/control.tsx | 24 +- .../components/workflow/operator/index.tsx | 78 +++- .../components/variable-modal.tsx | 2 +- .../panel/chat-variable-panel/index.tsx | 21 +- .../panel/debug-and-preview/chat-wrapper.tsx | 8 + .../workflow/panel/debug-and-preview/hooks.ts | 8 + .../panel/debug-and-preview/index.tsx | 152 +++---- web/app/components/workflow/panel/index.tsx | 63 ++- .../panel/version-history-panel/index.tsx | 14 +- .../components/workflow/run/result-panel.tsx | 4 +- .../workflow/debug/inspect-vars-slice.ts | 142 ++++++ .../store/workflow/debug/mock-data.ts | 72 +++ .../workflow/store/workflow/index.ts | 9 + .../workflow/store/workflow/layout-slice.ts | 44 ++ .../workflow/store/workflow/panel-slice.ts | 8 + web/app/components/workflow/types.ts | 8 +- web/app/components/workflow/utils/debug.ts | 25 + web/app/components/workflow/utils/workflow.ts | 9 +- .../workflow/variable-inspect/empty.tsx | 28 ++ .../workflow/variable-inspect/group.tsx | 174 +++++++ .../workflow/variable-inspect/index.tsx | 61 +++ .../workflow/variable-inspect/left.tsx | 111 +++++ .../workflow/variable-inspect/panel.tsx | 188 ++++++++ .../workflow/variable-inspect/right.tsx | 150 ++++++ .../workflow/variable-inspect/trigger.tsx | 113 +++++ .../workflow/variable-inspect/types.ts | 1 + .../workflow/variable-inspect/utils.tsx | 33 ++ .../variable-inspect/value-content.tsx | 225 +++++++++ web/i18n/en-US/workflow.ts | 31 ++ web/i18n/zh-Hans/workflow.ts | 31 ++ web/service/use-workflow.ts | 121 ++++- web/service/workflow.ts | 29 ++ web/types/workflow.ts | 53 ++- 122 files changed, 5888 insertions(+), 2061 deletions(-) create mode 100644 web/app/components/app-sidebar/app-sidebar-dropdown.tsx create mode 100644 web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts create mode 100644 web/app/components/workflow/hooks/use-inspect-vars-crud.ts create mode 100644 web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx delete mode 100644 web/app/components/workflow/nodes/_base/panel.tsx create mode 100644 web/app/components/workflow/nodes/agent/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/code/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/http/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/llm/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/loop/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/start/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts create mode 100644 web/app/components/workflow/nodes/tool/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts create mode 100644 web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts create mode 100644 web/app/components/workflow/store/workflow/debug/mock-data.ts create mode 100644 web/app/components/workflow/store/workflow/layout-slice.ts create mode 100644 web/app/components/workflow/utils/debug.ts create mode 100644 web/app/components/workflow/variable-inspect/empty.tsx create mode 100644 web/app/components/workflow/variable-inspect/group.tsx create mode 100644 web/app/components/workflow/variable-inspect/index.tsx create mode 100644 web/app/components/workflow/variable-inspect/left.tsx create mode 100644 web/app/components/workflow/variable-inspect/panel.tsx create mode 100644 web/app/components/workflow/variable-inspect/right.tsx create mode 100644 web/app/components/workflow/variable-inspect/trigger.tsx create mode 100644 web/app/components/workflow/variable-inspect/types.ts create mode 100644 web/app/components/workflow/variable-inspect/utils.tsx create mode 100644 web/app/components/workflow/variable-inspect/value-content.tsx diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index 7b6e66f7e..00e9658a2 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -39,16 +39,19 @@ import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigge export type IAppInfoProps = { expand: boolean + onlyShowDetail?: boolean + openState?: boolean + onDetailExpand?: (expand: boolean) => void } -const AppInfo = ({ expand }: IAppInfoProps) => { +const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailExpand }: IAppInfoProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const { replace } = useRouter() const { onPlanInfoChanged } = useProviderContext() const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(openState) const [showEditModal, setShowEditModal] = useState(false) const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) @@ -193,43 +196,48 @@ const AppInfo = ({ expand }: IAppInfoProps) => { return (
- + ) + } +
+ + )} setOpen(false)} + show={onlyShowDetail ? openState : open} + onClose={() => { + setOpen(false) + onDetailExpand?.(false) + }} className='absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl !p-0' >
@@ -258,6 +266,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { className='gap-[1px]' onClick={() => { setOpen(false) + onDetailExpand?.(false) setShowEditModal(true) }} > @@ -270,6 +279,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { className='gap-[1px]' onClick={() => { setOpen(false) + onDetailExpand?.(false) setShowDuplicateModal(true) }}> @@ -308,6 +318,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { &&
{ setOpen(false) + onDetailExpand?.(false) setShowImportDSLModal(true) }}> @@ -319,6 +330,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { &&
{ setOpen(false) + onDetailExpand?.(false) setShowSwitchModal(true) }}> @@ -345,6 +357,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { className='gap-0.5' onClick={() => { setOpen(false) + onDetailExpand?.(false) setShowConfirmDelete(true) }} > diff --git a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx new file mode 100644 index 000000000..b1da43ae1 --- /dev/null +++ b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx @@ -0,0 +1,125 @@ +import React, { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppContext } from '@/context/app-context' +import { + RiEqualizer2Line, + RiMenuLine, +} from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import AppIcon from '../base/app-icon' +import Divider from '../base/divider' +import AppInfo from './app-info' +import NavLink from './navLink' +import { useStore as useAppStore } from '@/app/components/app/store' +import type { NavIcon } from './navLink' +import cn from '@/utils/classnames' + +type Props = { + navigation: Array<{ + name: string + href: string + icon: NavIcon + selectedIcon: NavIcon + }> +} + +const AppSidebarDropdown = ({ navigation }: Props) => { + const { t } = useTranslation() + const { isCurrentWorkspaceEditor } = useAppContext() + const appDetail = useAppStore(state => state.appDetail) + const [detailExpand, setDetailExpand] = useState(false) + + const [open, doSetOpen] = useState(false) + const openRef = useRef(open) + const setOpen = useCallback((v: boolean) => { + doSetOpen(v) + openRef.current = v + }, [doSetOpen]) + const handleTrigger = useCallback(() => { + setOpen(!openRef.current) + }, [setOpen]) + + if (!appDetail) + return null + + return ( + <> +
+ + +
+ + +
+
+ +
+
+
{ + setDetailExpand(true) + setOpen(false) + }} + > +
+ +
+
+ +
+
+
+
+
+
{appDetail.name}
+
+
{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ + ) +} + +export default AppSidebarDropdown diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index f58985ed9..b6bfc0e9a 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -1,4 +1,5 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' +import { usePathname } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { RiLayoutLeft2Line, RiLayoutRight2Line } from '@remixicon/react' import NavLink from './navLink' @@ -6,8 +7,10 @@ import type { NavIcon } from './navLink' import AppBasic from './basic' import AppInfo from './app-info' import DatasetInfo from './dataset-info' +import AppSidebarDropdown from './app-sidebar-dropdown' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useStore as useAppStore } from '@/app/components/app/store' +import { useEventEmitterContextContext } from '@/context/event-emitter' import cn from '@/utils/classnames' export type IAppDetailNavProps = { @@ -39,6 +42,18 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati setAppSiderbarExpand(state === 'expand' ? 'collapse' : 'expand') } + // // Check if the current path is a workflow canvas & fullscreen + const pathname = usePathname() + const inWorkflowCanvas = pathname.endsWith('/workflow') + const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) + const { eventEmitter } = useEventEmitterContextContext() + + eventEmitter?.useSubscription((v: any) => { + if (v?.type === 'workflow-canvas-maximize') + setHideHeader(v.payload) + }) + useEffect(() => { if (appSidebarExpand) { localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand) @@ -46,6 +61,14 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati } }, [appSidebarExpand, setAppSiderbarExpand]) + if (inWorkflowCanvas && hideHeader) { + return ( +
+ +
+ ) +} + return (
{ const { t } = useTranslation() @@ -89,16 +91,18 @@ const FileUploaderInAttachment = ({ return (
-
- {options.map(renderOption)} -
+ {!isDisabled && ( +
+ {options.map(renderOption)} +
+ )}
{ files.map(file => ( handleRemoveFile(file.id)} onReUpload={() => handleReUploadFile(file.id)} @@ -114,18 +118,20 @@ type FileUploaderInAttachmentWrapperProps = { value?: FileEntity[] onChange: (files: FileEntity[]) => void fileConfig: FileUpload + isDisabled?: boolean } const FileUploaderInAttachmentWrapper = ({ value, onChange, fileConfig, + isDisabled, }: FileUploaderInAttachmentWrapperProps) => { return ( - + ) } diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts index 9b5a44948..e870f9eda 100644 --- a/web/app/components/base/file-uploader/utils.ts +++ b/web/app/components/base/file-uploader/utils.ts @@ -154,7 +154,7 @@ export const getProcessedFilesFromResponse = (files: FileResponse[]) => { transferMethod: fileItem.transfer_method, supportFileType: fileItem.type, uploadedId: fileItem.upload_file_id || fileItem.related_id, - url: fileItem.url, + url: fileItem.url || fileItem.remote_url, } }) } diff --git a/web/app/components/base/tab-header/index.tsx b/web/app/components/base/tab-header/index.tsx index 36dfa8c68..846277e5d 100644 --- a/web/app/components/base/tab-header/index.tsx +++ b/web/app/components/base/tab-header/index.tsx @@ -9,30 +9,34 @@ type Item = { isRight?: boolean icon?: React.ReactNode extra?: React.ReactNode + disabled?: boolean } export type ITabHeaderProps = { items: Item[] value: string + itemClassName?: string onChange: (value: string) => void } const TabHeader: FC = ({ items, value, + itemClassName, onChange, }) => { - const renderItem = ({ id, name, icon, extra }: Item) => ( + const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
onChange(id)} + onClick={() => !disabled && onChange(id)} > {icon || ''} -
{name}
+
{name}
{extra || ''}
) diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx index dd0ec77b8..013e25c8b 100644 --- a/web/app/components/header/header-wrapper.tsx +++ b/web/app/components/header/header-wrapper.tsx @@ -1,6 +1,8 @@ 'use client' +import React, { useState } from 'react' import { usePathname } from 'next/navigation' import s from './index.module.css' +import { useEventEmitterContextContext } from '@/context/event-emitter' import classNames from '@/utils/classnames' type HeaderWrapperProps = { @@ -12,6 +14,19 @@ const HeaderWrapper = ({ }: HeaderWrapperProps) => { const pathname = usePathname() const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools'].includes(pathname) + // // Check if the current path is a workflow canvas & fullscreen + const inWorkflowCanvas = pathname.endsWith('/workflow') + const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) + const { eventEmitter } = useEventEmitterContextContext() + + eventEmitter?.useSubscription((v: any) => { + if (v?.type === 'workflow-canvas-maximize') + setHideHeader(v.payload) + }) + + if (hideHeader && inWorkflowCanvas) + return null return (
{ + const workflowStore = useWorkflowStore() + const { setNodesWithInspectVars, appId } = workflowStore.getState() + const store = useStoreApi() + const invalidateConversationVarValues = useInvalidateConversationVarValues(appId) + const invalidateSysVarValues = useInvalidateSysVarValues(appId) + const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync() + + const setInspectVarsToStore = (inspectVars: VarInInspect[]) => { + const { getNodes } = store.getState() + const nodeArr = getNodes() + const nodesKeyValue: Record = {} + nodeArr.forEach((node) => { + nodesKeyValue[node.id] = node + }) + + const withValueNodeIds: Record = {} + inspectVars.forEach((varItem) => { + const nodeId = varItem.selector[0] + + const node = nodesKeyValue[nodeId] + if (!node) + return + withValueNodeIds[nodeId] = true + }) + const withValueNodes = Object.keys(withValueNodeIds).map((nodeId) => { + return nodesKeyValue[nodeId] + }) + + const res: NodeWithVar[] = withValueNodes.map((node) => { + const nodeId = node.id + const varsUnderTheNode = inspectVars.filter((varItem) => { + return varItem.selector[0] === nodeId + }) + const nodeWithVar = { + nodeId, + nodePayload: node.data, + nodeType: node.data.type, + title: node.data.title, + vars: varsUnderTheNode, + isSingRunRunning: false, + isValueFetched: false, + } + return nodeWithVar + }) + setNodesWithInspectVars(res) + } + + const fetchInspectVars = async () => { + invalidateConversationVarValues() + invalidateSysVarValues() + const data = await fetchAllInspectVars(appId) + setInspectVarsToStore(data) + handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status + } + return { + fetchInspectVars, + } +} + +export default useSetWorkflowVarsWithValue diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index e1c4c25a4..6d16dc5c4 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -17,7 +17,6 @@ import { } from '@/service/workflow' import type { FetchWorkflowDraftResponse } from '@/types/workflow' import { useWorkflowConfig } from '@/service/use-workflow' - export const useWorkflowInit = () => { const workflowStore = useWorkflowStore() const { diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 1e484d076..99b88238f 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -19,6 +19,8 @@ import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player import type { VersionHistory } from '@/types/workflow' import { noop } from 'lodash-es' import { useNodesSyncDraft } from './use-nodes-sync-draft' +import { useInvalidAllLastRun } from '@/service/use-workflow' +import useSetWorkflowVarsWithValue from './use-fetch-workflow-inspect-vars' export const useWorkflowRun = () => { const store = useStoreApi() @@ -28,6 +30,9 @@ export const useWorkflowRun = () => { const { doSyncWorkflowDraft } = useNodesSyncDraft() const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() const pathname = usePathname() + const appId = useAppStore.getState().appDetail?.id + const invalidAllLastRun = useInvalidAllLastRun(appId as string) + const { fetchInspectVars } = useSetWorkflowVarsWithValue() const { handleWorkflowStarted, @@ -140,11 +145,13 @@ export const useWorkflowRun = () => { clientHeight, } = workflowContainer! + const isInWorkflowDebug = appDetail?.mode === 'workflow' + let url = '' if (appDetail?.mode === 'advanced-chat') url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run` - if (appDetail?.mode === 'workflow') + if (isInWorkflowDebug) url = `/apps/${appDetail.id}/workflows/draft/run` const { @@ -189,6 +196,10 @@ export const useWorkflowRun = () => { if (onWorkflowFinished) onWorkflowFinished(params) + if (isInWorkflowDebug) { + fetchInspectVars() + invalidAllLastRun() + } }, onError: (params) => { handleWorkflowFailed() @@ -292,26 +303,7 @@ export const useWorkflowRun = () => { ...restCallback, }, ) - }, [ - store, - workflowStore, - doSyncWorkflowDraft, - handleWorkflowStarted, - handleWorkflowFinished, - handleWorkflowFailed, - handleWorkflowNodeStarted, - handleWorkflowNodeFinished, - handleWorkflowNodeIterationStarted, - handleWorkflowNodeIterationNext, - handleWorkflowNodeIterationFinished, - handleWorkflowNodeLoopStarted, - handleWorkflowNodeLoopNext, - handleWorkflowNodeLoopFinished, - handleWorkflowNodeRetry, - handleWorkflowTextChunk, - handleWorkflowTextReplace, - handleWorkflowAgentLog, - pathname], + }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace], ) const handleStopRun = useCallback((taskId: string) => { diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index b7432f120..304295cfb 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -31,6 +31,7 @@ type NodesExtraData = { getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[] getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[] checkValid: any + defaultRunInputData?: Record } export const NODES_EXTRA_DATA: Record = { [BlockEnum.Start]: { @@ -68,6 +69,7 @@ export const NODES_EXTRA_DATA: Record = { getAvailablePrevNodes: LLMDefault.getAvailablePrevNodes, getAvailableNextNodes: LLMDefault.getAvailableNextNodes, checkValid: LLMDefault.checkValid, + defaultRunInputData: LLMDefault.defaultRunInputData, }, [BlockEnum.KnowledgeRetrieval]: { author: 'Dify', diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index 4d1954587..afa4e6209 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -17,6 +17,8 @@ import { import Toast from '../../base/toast' import RestoringTitle from './restoring-title' import Button from '@/app/components/base/button' +import { useStore as useAppStore } from '@/app/components/app/store' +import { useInvalidAllLastRun } from '@/service/use-workflow' export type HeaderInRestoringProps = { onRestoreSettled?: () => void @@ -26,6 +28,12 @@ const HeaderInRestoring = ({ }: HeaderInRestoringProps) => { const { t } = useTranslation() const workflowStore = useWorkflowStore() + const appDetail = useAppStore.getState().appDetail + + const invalidAllLastRun = useInvalidAllLastRun(appDetail!.id) + const { + deleteAllInspectVars, + } = workflowStore.getState() const currentVersion = useStore(s => s.currentVersion) const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) @@ -61,7 +69,9 @@ const HeaderInRestoring = ({ onRestoreSettled?.() }, }) - }, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, onRestoreSettled, t]) + deleteAllInspectVars() + invalidAllLastRun() + }, [setShowWorkflowVersionHistoryPanel, workflowStore, handleSyncWorkflowDraft, deleteAllInspectVars, invalidAllLastRun, t, onRestoreSettled]) return ( <> diff --git a/web/app/components/workflow/header/index.tsx b/web/app/components/workflow/header/index.tsx index e5391afb0..771375347 100644 --- a/web/app/components/workflow/header/index.tsx +++ b/web/app/components/workflow/header/index.tsx @@ -1,3 +1,4 @@ +import { usePathname } from 'next/navigation' import { useWorkflowMode, } from '../hooks' @@ -6,7 +7,7 @@ import HeaderInNormal from './header-in-normal' import HeaderInHistory from './header-in-view-history' import type { HeaderInRestoringProps } from './header-in-restoring' import HeaderInRestoring from './header-in-restoring' - +import { useStore } from '../store' export type HeaderProps = { normal?: HeaderInNormalProps restoring?: HeaderInRestoringProps @@ -15,16 +16,20 @@ const Header = ({ normal: normalProps, restoring: restoringProps, }: HeaderProps) => { + const pathname = usePathname() + const inWorkflowCanvas = pathname.endsWith('/workflow') const { normal, restoring, viewHistory, } = useWorkflowMode() + const maximizeCanvas = useStore(s => s.maximizeCanvas) return (
+ {inWorkflowCanvas && maximizeCanvas &&
} { normal && ( { const { t } = useTranslation() @@ -27,6 +29,16 @@ const RunMode = memo(() => { const workflowRunningData = useStore(s => s.workflowRunningData) const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running + const handleStop = () => { + handleStopRun(workflowRunningData?.task_id || '') + } + + const { eventEmitter } = useEventEmitterContextContext() + eventEmitter?.useSubscription((v: any) => { + if (v.type === EVENT_WORKFLOW_STOP) + handleStop() + }) + return ( <>
{ isRunning && (
handleStopRun(workflowRunningData?.task_id || '')} + onClick={handleStop} >
diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts new file mode 100644 index 000000000..59cc98a17 --- /dev/null +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts @@ -0,0 +1,241 @@ +import { fetchNodeInspectVars } from '@/service/workflow' +import { useStore, useWorkflowStore } from '../store' +import type { ValueSelector } from '../types' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import { + useConversationVarValues, + useDeleteAllInspectorVars, + useDeleteInspectVar, + useDeleteNodeInspectorVars, + useEditInspectorVar, + useInvalidateConversationVarValues, + useInvalidateSysVarValues, + useLastRun, + useResetConversationVar, + useResetToLastRunValue, + useSysVarValues, +} from '@/service/use-workflow' +import { useCallback, useEffect, useState } from 'react' +import { isConversationVar, isENV, isSystemVar } from '../nodes/_base/components/variable/utils' +import produce from 'immer' +import type { Node } from '@/app/components/workflow/types' +import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync' +import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync' + +const useInspectVarsCrud = () => { + const workflowStore = useWorkflowStore() + const nodesWithInspectVars = useStore(s => s.nodesWithInspectVars) + const { + appId, + setNodeInspectVars, + setInspectVarValue, + renameInspectVarName: renameInspectVarNameInStore, + deleteAllInspectVars: deleteAllInspectVarsInStore, + deleteNodeInspectVars: deleteNodeInspectVarsInStore, + deleteInspectVar: deleteInspectVarInStore, + setNodesWithInspectVars, + resetToLastRunVar: resetToLastRunVarInStore, + } = workflowStore.getState() + + const { data: conversationVars } = useConversationVarValues(appId) + const invalidateConversationVarValues = useInvalidateConversationVarValues(appId) + const { mutateAsync: doResetConversationVar } = useResetConversationVar(appId) + const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(appId) + const { data: systemVars } = useSysVarValues(appId) + const invalidateSysVarValues = useInvalidateSysVarValues(appId) + + const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(appId) + const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(appId) + const { mutate: doDeleteInspectVar } = useDeleteInspectVar(appId) + + const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(appId) + const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() + const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() + const getNodeInspectVars = useCallback((nodeId: string) => { + const node = nodesWithInspectVars.find(node => node.nodeId === nodeId) + return node + }, [nodesWithInspectVars]) + + const getVarId = useCallback((nodeId: string, varName: string) => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + const varId = node.vars.find((varItem) => { + return varItem.selector[1] === varName + })?.id + return varId + }, [getNodeInspectVars]) + + const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + + const variable = node.vars.find((varItem) => { + return varItem.name === name + }) + return variable + }, [getNodeInspectVars]) + + const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => { + const isEnv = isENV([nodeId]) + if (isEnv) // always have value + return true + const isSys = isSystemVar([nodeId]) + if (isSys) + return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name) + const isChatVar = isConversationVar([nodeId]) + if (isChatVar) + return conversationVars.some(varItem => varItem.selector?.[1] === name) + return getInspectVar(nodeId, name) !== undefined + }, [getInspectVar]) + + const hasNodeInspectVars = useCallback((nodeId: string) => { + return !!getNodeInspectVars(nodeId) + }, [getNodeInspectVars]) + + const fetchInspectVarValue = async (selector: ValueSelector) => { + const nodeId = selector[0] + const isSystemVar = nodeId === 'sys' + const isConversationVar = nodeId === 'conversation' + if (isSystemVar) { + invalidateSysVarValues() + return + } + if (isConversationVar) { + invalidateConversationVarValues() + return + } + const vars = await fetchNodeInspectVars(appId, nodeId) + setNodeInspectVars(nodeId, vars) + } + + // after last run would call this + const appendNodeInspectVars = (nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { + const nodes = produce(nodesWithInspectVars, (draft) => { + const nodeInfo = allNodes.find(node => node.id === nodeId) + if (nodeInfo) { + const index = draft.findIndex(node => node.nodeId === nodeId) + if (index === -1) { + draft.push({ + nodeId, + nodeType: nodeInfo.data.type, + title: nodeInfo.data.title, + vars: payload, + }) + } + else { + draft[index].vars = payload + } + } + }) + setNodesWithInspectVars(nodes) + handleCancelNodeSuccessStatus(nodeId) + } + + const hasNodeInspectVar = (nodeId: string, varId: string) => { + const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId) + if(!targetNode || !targetNode.vars) + return false + return targetNode.vars.some(item => item.id === varId) + } + + const deleteInspectVar = async (nodeId: string, varId: string) => { + if(hasNodeInspectVar(nodeId, varId)) { + await doDeleteInspectVar(varId) + deleteInspectVarInStore(nodeId, varId) + } + } + + const resetConversationVar = async (varId: string) => { + await doResetConversationVar(varId) + invalidateConversationVarValues() + } + + const deleteNodeInspectorVars = async (nodeId: string) => { + if (hasNodeInspectVars(nodeId)) { + await doDeleteNodeInspectorVars(nodeId) + deleteNodeInspectVarsInStore(nodeId) + } + } + + const deleteAllInspectorVars = async () => { + await doDeleteAllInspectorVars() + await invalidateConversationVarValues() + await invalidateSysVarValues() + deleteAllInspectVarsInStore() + handleEdgeCancelRunningStatus() + } + + const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => { + await doEditInspectorVar({ + varId, + value, + }) + setInspectVarValue(nodeId, varId, value) + if (nodeId === VarInInspectType.conversation) + invalidateConversationVarValues() + if (nodeId === VarInInspectType.system) + invalidateSysVarValues() + }, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, setInspectVarValue]) + + const [currNodeId, setCurrNodeId] = useState(null) + const [currEditVarId, setCurrEditVarId] = useState(null) + const { data } = useLastRun(appId, currNodeId || '', !!currNodeId) + useEffect(() => { + if (data && currNodeId && currEditVarId) { + const inspectVar = getNodeInspectVars(currNodeId)?.vars?.find(item => item.id === currEditVarId) + resetToLastRunVarInStore(currNodeId, currEditVarId, data.outputs?.[inspectVar?.selector?.[1] || '']) + } + }, [data, currNodeId, currEditVarId, getNodeInspectVars, editInspectVarValue, resetToLastRunVarInStore]) + + const renameInspectVarName = async (nodeId: string, oldName: string, newName: string) => { + const varId = getVarId(nodeId, oldName) + if (!varId) + return + + const newSelector = [nodeId, newName] + await doEditInspectorVar({ + varId, + name: newName, + }) + renameInspectVarNameInStore(nodeId, varId, newSelector) + } + + const isInspectVarEdited = useCallback((nodeId: string, name: string) => { + const inspectVar = getInspectVar(nodeId, name) + if (!inspectVar) + return false + + return inspectVar.edited + }, [getInspectVar]) + + const resetToLastRunVar = async (nodeId: string, varId: string) => { + await doResetToLastRunValue(varId) + setCurrNodeId(nodeId) + setCurrEditVarId(varId) + } + + return { + conversationVars: conversationVars || [], + systemVars: systemVars || [], + nodesWithInspectVars, + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + } +} + +export default useInspectVarsCrud diff --git a/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts b/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts index 7fbf0ce86..e01609cdb 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react' import produce from 'immer' import { useStoreApi } from 'reactflow' +import { NodeRunningStatus } from '../types' export const useNodesInteractionsWithoutSync = () => { const store = useStoreApi() @@ -21,7 +22,41 @@ export const useNodesInteractionsWithoutSync = () => { setNodes(newNodes) }, [store]) + const handleCancelAllNodeSuccessStatus = useCallback(() => { + const { + getNodes, + setNodes, + } = store.getState() + + const nodes = getNodes() + const newNodes = produce(nodes, (draft) => { + draft.forEach((node) => { + if(node.data._runningStatus === NodeRunningStatus.Succeeded) + node.data._runningStatus = undefined + }) + }) + setNodes(newNodes) + }, [store]) + + const handleCancelNodeSuccessStatus = useCallback((nodeId: string) => { + const { + getNodes, + setNodes, + } = store.getState() + + const newNodes = produce(getNodes(), (draft) => { + const node = draft.find(n => n.id === nodeId) + if (node && node.data._runningStatus === NodeRunningStatus.Succeeded) { + node.data._runningStatus = undefined + node.data._waitingRun = false + } + }) + setNodes(newNodes) + }, [store]) + return { handleNodeCancelRunningStatus, + handleCancelAllNodeSuccessStatus, + handleCancelNodeSuccessStatus, } } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 94b10c992..b598951ad 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -60,6 +60,7 @@ import { useWorkflowReadOnly, } from './use-workflow' import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' +import useInspectVarsCrud from './use-inspect-vars-crud' export const useNodesInteractions = () => { const { t } = useTranslation() @@ -288,7 +289,9 @@ export const useNodesInteractions = () => { setEdges(newEdges) }, [store, workflowStore, getNodesReadOnly]) - const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => { + const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean, initShowLastRunTab?: boolean) => { + if(initShowLastRunTab) + workflowStore.setState({ initShowLastRunTab: true }) const { getNodes, setNodes, @@ -530,6 +533,8 @@ export const useNodesInteractions = () => { setEnteringNodePayload(undefined) }, [store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow]) + const { deleteNodeInspectorVars } = useInspectVarsCrud() + const handleNodeDelete = useCallback((nodeId: string) => { if (getNodesReadOnly()) return @@ -551,6 +556,7 @@ export const useNodesInteractions = () => { if (currentNode.data.type === BlockEnum.Start) return + deleteNodeInspectorVars(nodeId) if (currentNode.data.type === BlockEnum.Iteration) { const iterationChildren = nodes.filter(node => node.parentId === currentNode.id) @@ -655,7 +661,7 @@ export const useNodesInteractions = () => { else saveStateToHistory(WorkflowHistoryEvent.NodeDelete) - }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) + }, [getNodesReadOnly, store, deleteNodeInspectorVars, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) const handleNodeAdd = useCallback(( { diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 8b1003e89..118ec9405 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -11,6 +11,7 @@ import { useEdgesInteractions, useNodesInteractions, useNodesSyncDraft, + useWorkflowCanvasMaximize, useWorkflowMoveMode, useWorkflowOrganize, useWorkflowStartRun, @@ -35,6 +36,7 @@ export const useShortcuts = (): void => { handleModePointer, } = useWorkflowMoveMode() const { handleLayout } = useWorkflowOrganize() + const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize() const { zoomTo, @@ -145,6 +147,16 @@ export const useShortcuts = (): void => { } }, { exactMatch: true, useCapture: true }) + useKeyPress('f', (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleToggleMaximizeCanvas() + } + }, { + exactMatch: true, + useCapture: true, + }) + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => { if (shouldHandleShortcut(e)) { e.preventDefault() diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 636d3b94f..d8653a594 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -401,3 +401,29 @@ export const useDSL = () => { handleExportDSL, } } + +export const useWorkflowCanvasMaximize = () => { + const { eventEmitter } = useEventEmitterContextContext() + + const maximizeCanvas = useStore(s => s.maximizeCanvas) + const setMaximizeCanvas = useStore(s => s.setMaximizeCanvas) + const { + getNodesReadOnly, + } = useNodesReadOnly() + + const handleToggleMaximizeCanvas = useCallback(() => { + if (getNodesReadOnly()) + return + + setMaximizeCanvas(!maximizeCanvas) + localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas)) + eventEmitter?.emit({ + type: 'workflow-canvas-maximize', + payload: !maximizeCanvas, + } as any) + }, [eventEmitter, getNodesReadOnly, maximizeCanvas, setMaximizeCanvas]) + + return { + handleToggleMaximizeCanvas, + } +} diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 99dce4dc1..1b9817815 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -59,10 +59,6 @@ export const useWorkflow = () => { const store = useStoreApi() const workflowStore = useWorkflowStore() const nodesExtraData = useNodesExtraData() - const setPanelWidth = useCallback((width: number) => { - localStorage.setItem('workflow-node-panel-width', `${width}`) - workflowStore.setState({ panelWidth: width }) - }, [workflowStore]) const getTreeLeafNodes = useCallback((nodeId: string) => { const { @@ -399,7 +395,6 @@ export const useWorkflow = () => { }, [store]) return { - setPanelWidth, getTreeLeafNodes, getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent, @@ -497,6 +492,8 @@ export const useToolIcon = (data: Node['data']) => { const customTools = useStore(s => s.customTools) const workflowTools = useStore(s => s.workflowTools) const toolIcon = useMemo(() => { + if(!data) + return '' if (data.type === BlockEnum.Tool) { let targetTools = buildInTools if (data.provider_type === CollectionType.builtIn) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 549117faf..429d07853 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -5,6 +5,7 @@ import { memo, useCallback, useEffect, + useMemo, useRef, } from 'react' import { setAutoFreeze } from 'immer' @@ -56,6 +57,7 @@ import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' import CustomSimpleNode from './simple-node' import { CUSTOM_SIMPLE_NODE } from './simple-node/constants' import Operator from './operator' +import Control from './operator/control' import CustomEdge from './custom-edge' import CustomConnectionLine from './custom-connection-line' import HelpLine from './help-line' @@ -80,6 +82,7 @@ import Confirm from '@/app/components/base/confirm' import DatasetsDetailProvider from './datasets-detail-store/provider' import { HooksStoreContextProvider } from './hooks-store' import type { Shape as HooksStoreShape } from './hooks-store' +import useSetWorkflowVarsWithValue from '../workflow-app/hooks/use-fetch-workflow-inspect-vars' const nodeTypes = { [CUSTOM_NODE]: CustomNode, @@ -114,6 +117,32 @@ export const Workflow: FC = memo(({ const controlMode = useStore(s => s.controlMode) const nodeAnimation = useStore(s => s.nodeAnimation) const showConfirm = useStore(s => s.showConfirm) + const workflowCanvasHeight = useStore(s => s.workflowCanvasHeight) + const bottomPanelHeight = useStore(s => s.bottomPanelHeight) + const setWorkflowCanvasWidth = useStore(s => s.setWorkflowCanvasWidth) + const setWorkflowCanvasHeight = useStore(s => s.setWorkflowCanvasHeight) + const controlHeight = useMemo(() => { + if (!workflowCanvasHeight) + return '100%' + return workflowCanvasHeight - bottomPanelHeight + }, [workflowCanvasHeight, bottomPanelHeight]) + + // update workflow Canvas width and height + useEffect(() => { + if (workflowContainerRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize, blockSize } = entry.borderBoxSize[0] + setWorkflowCanvasWidth(inlineSize) + setWorkflowCanvasHeight(blockSize) + } + }) + resizeContainerObserver.observe(workflowContainerRef.current) + return () => { + resizeContainerObserver.disconnect() + } + } + }, [setWorkflowCanvasHeight, setWorkflowCanvasWidth]) const { setShowConfirm, @@ -245,6 +274,11 @@ export const Workflow: FC = memo(({ }) useShortcuts() + const { fetchInspectVars } = useSetWorkflowVarsWithValue() + useEffect(() => { + fetchInspectVars() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) const store = useStoreApi() if (process.env.NODE_ENV === 'development') { @@ -267,6 +301,12 @@ export const Workflow: FC = memo(({ > +
+ +
diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx index 9f415adda..269f5e0a9 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx @@ -95,6 +95,7 @@ const FormItem: FC = ({ const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type) const isContext = type === InputVarType.contexts const isIterator = type === InputVarType.iterator + const isIteratorItemFile = isIterator && payload.isFileItem const singleFileValue = useMemo(() => { if (payload.variable === '#files#') return value?.[0] || [] @@ -202,12 +203,12 @@ const FormItem: FC = ({ }} /> )} - {(type === InputVarType.multiFiles) && ( + {(type === InputVarType.multiFiles || isIteratorItemFile) && ( onChange(files)} fileConfig={{ - allowed_file_types: inStepRun + allowed_file_types: (inStepRun || isIteratorItemFile) ? [ SupportUploadFileTypes.image, SupportUploadFileTypes.document, @@ -215,7 +216,7 @@ const FormItem: FC = ({ SupportUploadFileTypes.video, ] : payload.allowed_file_types, - allowed_file_extensions: inStepRun + allowed_file_extensions: (inStepRun || isIteratorItemFile) ? [ ...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.document], @@ -223,8 +224,8 @@ const FormItem: FC = ({ ...FILE_EXTS[SupportUploadFileTypes.video], ] : payload.allowed_file_extensions, - allowed_file_upload_methods: inStepRun ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods, - number_limits: inStepRun ? 5 : payload.max_length, + allowed_file_upload_methods: (inStepRun || isIteratorItemFile) ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods, + number_limits: (inStepRun || isIteratorItemFile) ? 5 : payload.max_length, fileUploadConfig: fileSettings?.fileUploadConfig, }} /> @@ -272,7 +273,7 @@ const FormItem: FC = ({ } { - isIterator && ( + (isIterator && !isIteratorItemFile) && (
{(value || []).map((item: any, index: number) => ( = ({ } }, [valuesRef, onChange, mapKeysWithSameValueSelector]) const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(inputs[0]?.type) + const isIteratorItemFile = inputs[0]?.type === InputVarType.iterator && inputs[0]?.isFileItem + const isContext = inputs[0]?.type === InputVarType.contexts const handleAddContext = useCallback(() => { const newValues = produce(values, (draft: any) => { const key = inputs[0].variable + if (!draft[key]) + draft[key] = [] draft[key].push(isContext ? RETRIEVAL_OUTPUT_STRUCT : '') }) onChange(newValues) @@ -75,7 +79,7 @@ const Form: FC = ({ {label && (
{label}
- {isArrayLikeType && ( + {isArrayLikeType && !isIteratorItemFile && ( )}
diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx index ad8d0b9c6..11bd5156e 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx @@ -1,30 +1,23 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { - RiCloseLine, - RiLoader2Line, -} from '@remixicon/react' import type { Props as FormProps } from './form' import Form from './form' import cn from '@/utils/classnames' import Button from '@/app/components/base/button' -import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import Split from '@/app/components/workflow/nodes/_base/components/split' -import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import { InputVarType } from '@/app/components/workflow/types' import Toast from '@/app/components/base/toast' import { TransferMethod } from '@/types/app' import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' -import type { BlockEnum } from '@/app/components/workflow/types' +import type { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import type { Emoji } from '@/app/components/tools/types' import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel' -import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' - +import PanelWrap from './panel-wrap' const i18nPrefix = 'workflow.singleRun' -type BeforeRunFormProps = { +export type BeforeRunFormProps = { nodeName: string nodeType?: BlockEnum toolIcon?: string | Emoji @@ -32,12 +25,15 @@ type BeforeRunFormProps = { onRun: (submitData: Record) => void onStop: () => void runningStatus: NodeRunningStatus - result?: React.JSX.Element forms: FormProps[] showSpecialResultPanel?: boolean + existVarValuesInForms: Record[] + filteredExistVarForms: FormProps[] } & Partial function formatValue(value: string | any, type: InputVarType) { + if(value === undefined || value === null) + return value if (type === InputVarType.number) return Number.parseFloat(value) if (type === InputVarType.json) @@ -53,6 +49,8 @@ function formatValue(value: string | any, type: InputVarType) { if (type === InputVarType.singleFile) { if (Array.isArray(value)) return getProcessedFiles(value) + if (!value) + return undefined return getProcessedFiles([value])[0] } @@ -60,22 +58,17 @@ function formatValue(value: string | any, type: InputVarType) { } const BeforeRunForm: FC = ({ nodeName, - nodeType, - toolIcon, onHide, onRun, - onStop, - runningStatus, - result, forms, - showSpecialResultPanel, - ...restResultPanelParams + filteredExistVarForms, + existVarValuesInForms, }) => { const { t } = useTranslation() - const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed || runningStatus === NodeRunningStatus.Exception - const isRunning = runningStatus === NodeRunningStatus.Running const isFileLoaded = (() => { + if (!forms || forms.length === 0) + return true // system files const filesForm = forms.find(item => !!item.values['#files#']) if (!filesForm) @@ -87,12 +80,14 @@ const BeforeRunForm: FC = ({ return true })() - const handleRun = useCallback(() => { + const handleRun = () => { let errMsg = '' - forms.forEach((form) => { + forms.forEach((form, i) => { + const existVarValuesInForm = existVarValuesInForms[i] + form.inputs.forEach((input) => { const value = form.values[input.variable] as any - if (!errMsg && input.required && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0))) + if (!errMsg && input.required && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0))) errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label }) if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) { @@ -137,69 +132,45 @@ const BeforeRunForm: FC = ({ } onRun(submitData) - }, [forms, onRun, t]) + } + const hasRun = useRef(false) + useEffect(() => { + // React 18 run twice in dev mode + if(hasRun.current) + return + hasRun.current = true + if(filteredExistVarForms.length === 0) + onRun({}) + }, [filteredExistVarForms, onRun]) + + if(filteredExistVarForms.length === 0) + return null + return ( -
-
-
-
- {t(`${i18nPrefix}.testRun`)} {nodeName} -
-
{ - onHide() - }}> - -
+ +
+
+ {filteredExistVarForms.map((form, index) => ( +
+
+ {index < forms.length - 1 && } +
+ ))} +
+
+
- { - showSpecialResultPanel && ( -
- -
- ) - } - { - !showSpecialResultPanel && ( -
-
- {forms.map((form, index) => ( -
- - {index < forms.length - 1 && } -
- ))} -
-
- {isRunning && ( -
- -
- )} - -
- {isRunning && ( - - )} - {isFinished && ( - <> - {result} - - )} -
- ) - }
-
+ ) } export default React.memo(BeforeRunForm) diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx new file mode 100644 index 000000000..7312adf6c --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx @@ -0,0 +1,41 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, +} from '@remixicon/react' + +const i18nPrefix = 'workflow.singleRun' + +export type Props = { + nodeName: string + onHide: () => void + children: React.ReactNode +} + +const PanelWrap: FC = ({ + nodeName, + onHide, + children, +}) => { + const { t } = useTranslation() + return ( +
+
+
+
+ {t(`${i18nPrefix}.testRun`)} {nodeName} +
+
{ + onHide() + }}> + +
+
+ {children} +
+
+ ) +} +export default React.memo(PanelWrap) diff --git a/web/app/components/workflow/nodes/_base/components/node-control.tsx b/web/app/components/workflow/nodes/_base/components/node-control.tsx index a85c41741..5b92b7b6b 100644 --- a/web/app/components/workflow/nodes/_base/components/node-control.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-control.tsx @@ -13,7 +13,7 @@ import { useNodesInteractions, useNodesSyncDraft, } from '../../../hooks' -import type { Node } from '../../../types' +import { type Node, NodeRunningStatus } from '../../../types' import { canRunBySingle } from '../../../utils' import PanelOperator from './panel-operator' import { @@ -31,11 +31,12 @@ const NodeControl: FC = ({ const { handleNodeDataUpdate } = useNodeDataUpdate() const { handleNodeSelect } = useNodesInteractions() const { handleSyncWorkflowDraft } = useNodesSyncDraft() - + const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running const handleOpenChange = useCallback((newOpen: boolean) => { setOpen(newOpen) }, []) + const isChildNode = !!(data.isInIteration || data.isInLoop) return (
= ({ onClick={e => e.stopPropagation()} > { - canRunBySingle(data.type) && ( + canRunBySingle(data.type, isChildNode) && (
{ + const nextData: Record = { + _isSingleRun: !isSingleRunning, + } + if(isSingleRunning) + nextData._singleRunningStatus = undefined + handleNodeDataUpdate({ id, - data: { - _isSingleRun: !data._isSingleRun, - }, + data: nextData, }) handleNodeSelect(id) - if (!data._isSingleRun) - handleSyncWorkflowDraft(true) }} > { - data._isSingleRun + isSingleRunning ? : ( { - (showChangeBlock || canRunBySingle(data.type)) && ( + (showChangeBlock || canRunBySingle(data.type, isChildNode)) && ( <>
{ - canRunBySingle(data.type) && ( + canRunBySingle(data.type, isChildNode) && (
= ({ + id, + data, + children, + position, + width, + height, +}) => { + const { t } = useTranslation() + const { showMessageLogModal } = useAppStore(useShallow(state => ({ + showMessageLogModal: state.showMessageLogModal, + }))) + const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running + + const showSingleRunPanel = useStore(s => s.showSingleRunPanel) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const nodePanelWidth = useStore(s => s.nodePanelWidth) + const otherPanelWidth = useStore(s => s.otherPanelWidth) + const setNodePanelWidth = useStore(s => s.setNodePanelWidth) + + const maxNodePanelWidth = useMemo(() => { + if (!workflowCanvasWidth) + return 720 + if (!otherPanelWidth) + return workflowCanvasWidth - 400 + + return workflowCanvasWidth - otherPanelWidth - 400 + }, [workflowCanvasWidth, otherPanelWidth]) + + const updateNodePanelWidth = useCallback((width: number) => { + // Ensure the width is within the min and max range + const newValue = Math.min(Math.max(width, 400), maxNodePanelWidth) + localStorage.setItem('workflow-node-panel-width', `${newValue}`) + setNodePanelWidth(newValue) + }, [maxNodePanelWidth, setNodePanelWidth]) + + const handleResize = useCallback((width: number) => { + updateNodePanelWidth(width) + }, [updateNodePanelWidth]) + + const { + triggerRef, + containerRef, + } = useResizePanel({ + direction: 'horizontal', + triggerDirection: 'left', + minWidth: 400, + maxWidth: maxNodePanelWidth, + onResize: debounce(handleResize), + }) + + const debounceUpdate = debounce(updateNodePanelWidth) + useEffect(() => { + if (!workflowCanvasWidth) + return + if (workflowCanvasWidth - 400 <= nodePanelWidth + otherPanelWidth) + debounceUpdate(workflowCanvasWidth - 400 - otherPanelWidth) + }, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, updateNodePanelWidth]) + + const { handleNodeSelect } = useNodesInteractions() + const { nodesReadOnly } = useNodesReadOnly() + const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop) + const toolIcon = useToolIcon(data) + + const { saveStateToHistory } = useWorkflowHistory() + + const { + handleNodeDataUpdate, + handleNodeDataUpdateWithSyncDraft, + } = useNodeDataUpdate() + + const handleTitleBlur = useCallback((title: string) => { + handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) + saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) + const handleDescriptionChange = useCallback((desc: string) => { + handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) + saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) + + const isChildNode = !!(data.isInIteration || data.isInLoop) + const isSupportSingleRun = canRunBySingle(data.type, isChildNode) + const appDetail = useAppStore(state => state.appDetail) + + const hasClickRunning = useRef(false) + const [isPaused, setIsPaused] = useState(false) + + useEffect(() => { + if(data._singleRunningStatus === NodeRunningStatus.Running) { + hasClickRunning.current = true + setIsPaused(false) + } + else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) { + setIsPaused(true) + hasClickRunning.current = false + } + }, [data]) + + const updateNodeRunningStatus = useCallback((status: NodeRunningStatus) => { + handleNodeDataUpdate({ + id, + data: { + ...data, + _singleRunningStatus: status, + }, + }) + }, [handleNodeDataUpdate, id, data]) + + useEffect(() => { + // console.log(`id changed: ${id}, hasClickRunning: ${hasClickRunning.current}`) + hasClickRunning.current = false + }, [id]) + + const { + isShowSingleRun, + hideSingleRun, + runningStatus, + handleStop, + runInputData, + runInputDataRef, + runResult, + getInputVars, + toVarInputs, + tabType, + isRunAfterSingleRun, + setTabType, + singleRunParams, + nodeInfo, + setRunInputData, + handleSingleRun, + handleRunWithParams, + getExistVarValuesInForms, + getFilteredExistVarForms, + } = useLastRun({ + id, + data, + defaultRunInputData: NODES_EXTRA_DATA[data.type]?.defaultRunInputData || {}, + isPaused, + }) + + useEffect(() => { + setIsPaused(false) + }, [tabType]) + + const logParams = useLogs() + const passedLogParams = (() => { + if ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type)) + return logParams + + return {} + })() + + if(logParams.showSpecialResultPanel) { + return ( +
+
+ +
+ +
+
+
+
+ ) + } + + if (isShowSingleRun) { + return ( +
+
+ +
+
+ ) + } + + return ( +
+
+
+
+
+
+
+ + +
+ { + isSupportSingleRun && !nodesReadOnly && ( + +
{ + if(isSingleRunning) { + handleNodeDataUpdate({ + id, + data: { + _isSingleRun: false, + _singleRunningStatus: undefined, + }, + }) + } + else { + handleSingleRun() + } + }} + > + { + isSingleRunning ? + : + } +
+
+ ) + } + + + +
+
handleNodeSelect(id, true)} + > + +
+
+
+
+ +
+
+ +
+ +
+ + {tabType === TabType.settings && ( + <> +
+ {cloneElement(children as any, { + id, + data, + panelProps: { + getInputVars, + toVarInputs, + runInputData, + setRunInputData, + runResult, + runInputDataRef, + }, + })} +
+ + { + hasRetryNode(data.type) && ( + + ) + } + { + hasErrorHandleNode(data.type) && ( + + ) + } + { + !!availableNextBlocks.length && ( +
+
+ {t('workflow.panel.nextStep').toLocaleUpperCase()} +
+
+ {t('workflow.panel.addNextStep')} +
+ +
+ ) + } + + )} + + {tabType === TabType.lastRun && ( + + )} +
+
+ ) +} + +export default memo(BasePanel) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx new file mode 100644 index 000000000..a02998781 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx @@ -0,0 +1,126 @@ +'use client' +import type { ResultPanelProps } from '@/app/components/workflow/run/result-panel' +import ResultPanel from '@/app/components/workflow/run/result-panel' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import type { FC } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import NoData from './no-data' +import { useLastRun } from '@/service/use-workflow' +import { RiLoader2Line } from '@remixicon/react' +import type { NodeTracing } from '@/types/workflow' + +type Props = { + appId: string + nodeId: string + canSingleRun: boolean + isRunAfterSingleRun: boolean + updateNodeRunningStatus: (status: NodeRunningStatus) => void + nodeInfo?: NodeTracing + runningStatus?: NodeRunningStatus + onSingleRunClicked: () => void + singleRunResult?: NodeTracing + isPaused?: boolean +} & Partial + +const LastRun: FC = ({ + appId, + nodeId, + canSingleRun, + isRunAfterSingleRun, + updateNodeRunningStatus, + nodeInfo, + runningStatus: oneStepRunRunningStatus, + onSingleRunClicked, + singleRunResult, + isPaused, + ...otherResultPanelProps +}) => { + const isOneStepRunSucceed = oneStepRunRunningStatus === NodeRunningStatus.Succeeded + const isOneStepRunFailed = oneStepRunRunningStatus === NodeRunningStatus.Failed + // hide page and return to page would lost the oneStepRunRunningStatus + const [hidePageOneStepFinishedStatus, setHidePageOneStepFinishedStatus] = React.useState(null) + const [pageHasHide, setPageHasHide] = useState(false) + const [pageShowed, setPageShowed] = useState(false) + + const hidePageOneStepRunFinished = [NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(hidePageOneStepFinishedStatus!) + const canRunLastRun = !isRunAfterSingleRun || isOneStepRunSucceed || isOneStepRunFailed || (pageHasHide && hidePageOneStepRunFinished) + const { data: lastRunResult, isFetching, error } = useLastRun(appId, nodeId, canRunLastRun) + const isRunning = useMemo(() => { + if(isPaused) + return false + + if(!isRunAfterSingleRun) + return isFetching + return [NodeRunningStatus.Running, NodeRunningStatus.NotStart].includes(oneStepRunRunningStatus!) + }, [isFetching, isPaused, isRunAfterSingleRun, oneStepRunRunningStatus]) + + const noLastRun = (error as any)?.status === 404 + const runResult = (canRunLastRun ? lastRunResult : singleRunResult) || lastRunResult || {} + + const resetHidePageStatus = useCallback(() => { + setPageHasHide(false) + setPageShowed(false) + setHidePageOneStepFinishedStatus(null) + }, []) + useEffect(() => { + if (pageShowed && hidePageOneStepFinishedStatus && (!oneStepRunRunningStatus || oneStepRunRunningStatus === NodeRunningStatus.NotStart)) { + updateNodeRunningStatus(hidePageOneStepFinishedStatus) + resetHidePageStatus() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus]) + + useEffect(() => { + if([NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(oneStepRunRunningStatus!)) + setHidePageOneStepFinishedStatus(oneStepRunRunningStatus!) + }, [oneStepRunRunningStatus]) + + useEffect(() => { + resetHidePageStatus() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodeId]) + + const handlePageVisibilityChange = useCallback(() => { + if (document.visibilityState === 'hidden') + setPageHasHide(true) + else + setPageShowed(true) + }, []) + useEffect(() => { + document.addEventListener('visibilitychange', handlePageVisibilityChange) + + return () => { + document.removeEventListener('visibilitychange', handlePageVisibilityChange) + } + }, [handlePageVisibilityChange]) + + if (isFetching && !isRunAfterSingleRun) { + return ( +
+ +
) + } + + if (isRunning) + return + + if (!isPaused && (noLastRun || !runResult)) { + return ( + + ) + } + return ( +
+ +
+ ) +} +export default React.memo(LastRun) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx new file mode 100644 index 000000000..ad0058efa --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx @@ -0,0 +1,36 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time' +import Button from '@/app/components/base/button' +import { RiPlayLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +type Props = { + canSingleRun: boolean + onSingleRun: () => void +} + +const NoData: FC = ({ + canSingleRun, + onSingleRun, +}) => { + const { t } = useTranslation() + return ( +
+ +
{t('workflow.debug.noData.description')}
+ {canSingleRun && ( + + )} +
+ ) +} +export default React.memo(NoData) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts new file mode 100644 index 000000000..014707cdf --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -0,0 +1,330 @@ +import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' +import type { Params as OneStepRunParams } from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' +import { useCallback, useEffect, useState } from 'react' +import { TabType } from '../tab' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import useStartSingleRunFormParams from '@/app/components/workflow/nodes/start/use-single-run-form-params' +import useLLMSingleRunFormParams from '@/app/components/workflow/nodes/llm/use-single-run-form-params' +import useKnowledgeRetrievalSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params' +import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params' +import useTemplateTransformSingleRunFormParams from '@/app/components/workflow/nodes/template-transform/use-single-run-form-params' +import useQuestionClassifierSingleRunFormParams from '@/app/components/workflow/nodes/question-classifier/use-single-run-form-params' +import useParameterExtractorSingleRunFormParams from '@/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params' +import useHttpRequestSingleRunFormParams from '@/app/components/workflow/nodes/http/use-single-run-form-params' +import useToolSingleRunFormParams from '@/app/components/workflow/nodes/tool/use-single-run-form-params' +import useIterationSingleRunFormParams from '@/app/components/workflow/nodes/iteration/use-single-run-form-params' +import useAgentSingleRunFormParams from '@/app/components/workflow/nodes/agent/use-single-run-form-params' +import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params' +import useLoopSingleRunFormParams from '@/app/components/workflow/nodes/loop/use-single-run-form-params' +import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params' +import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params' +import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params' + +import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more' +import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config' + +// import +import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' +import { BlockEnum } from '@/app/components/workflow/types' +import { + useNodesSyncDraft, +} from '@/app/components/workflow/hooks' +import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' +import { useInvalidLastRun } from '@/service/use-workflow' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' + +const singleRunFormParamsHooks: Record = { + [BlockEnum.LLM]: useLLMSingleRunFormParams, + [BlockEnum.KnowledgeRetrieval]: useKnowledgeRetrievalSingleRunFormParams, + [BlockEnum.Code]: useCodeSingleRunFormParams, + [BlockEnum.TemplateTransform]: useTemplateTransformSingleRunFormParams, + [BlockEnum.QuestionClassifier]: useQuestionClassifierSingleRunFormParams, + [BlockEnum.HttpRequest]: useHttpRequestSingleRunFormParams, + [BlockEnum.Tool]: useToolSingleRunFormParams, + [BlockEnum.ParameterExtractor]: useParameterExtractorSingleRunFormParams, + [BlockEnum.Iteration]: useIterationSingleRunFormParams, + [BlockEnum.Agent]: useAgentSingleRunFormParams, + [BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams, + [BlockEnum.Loop]: useLoopSingleRunFormParams, + [BlockEnum.Start]: useStartSingleRunFormParams, + [BlockEnum.IfElse]: useIfElseSingleRunFormParams, + [BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams, + [BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams, + [BlockEnum.VariableAssigner]: undefined, + [BlockEnum.End]: undefined, + [BlockEnum.Answer]: undefined, + [BlockEnum.ListFilter]: undefined, + [BlockEnum.IterationStart]: undefined, + [BlockEnum.LoopStart]: undefined, + [BlockEnum.LoopEnd]: undefined, +} + +const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => { + return (params: any) => { + return singleRunFormParamsHooks[nodeType]?.(params) || {} + } +} + +const getDataForCheckMoreHooks: Record = { + [BlockEnum.Tool]: useToolGetDataForCheckMore, + [BlockEnum.LLM]: undefined, + [BlockEnum.KnowledgeRetrieval]: undefined, + [BlockEnum.Code]: undefined, + [BlockEnum.TemplateTransform]: undefined, + [BlockEnum.QuestionClassifier]: undefined, + [BlockEnum.HttpRequest]: undefined, + [BlockEnum.ParameterExtractor]: undefined, + [BlockEnum.Iteration]: undefined, + [BlockEnum.Agent]: undefined, + [BlockEnum.DocExtractor]: undefined, + [BlockEnum.Loop]: undefined, + [BlockEnum.Start]: undefined, + [BlockEnum.IfElse]: undefined, + [BlockEnum.VariableAggregator]: undefined, + [BlockEnum.End]: undefined, + [BlockEnum.Answer]: undefined, + [BlockEnum.VariableAssigner]: undefined, + [BlockEnum.ListFilter]: undefined, + [BlockEnum.IterationStart]: undefined, + [BlockEnum.Assigner]: undefined, + [BlockEnum.LoopStart]: undefined, + [BlockEnum.LoopEnd]: undefined, +} + +const useGetDataForCheckMoreHooks = (nodeType: BlockEnum) => { + return (id: string, payload: CommonNodeType) => { + return getDataForCheckMoreHooks[nodeType]?.({ id, payload }) || { + getData: () => { + return {} + }, + } + } +} + +type Params = Omit, 'isRunAfterSingleRun'> +const useLastRun = ({ + ...oneStepRunParams +}: Params) => { + const { conversationVars, systemVars, hasSetInspectVar } = useInspectVarsCrud() + const blockType = oneStepRunParams.data.type + const isStartNode = blockType === BlockEnum.Start + const isIterationNode = blockType === BlockEnum.Iteration + const isLoopNode = blockType === BlockEnum.Loop + const isAggregatorNode = blockType === BlockEnum.VariableAggregator + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { + getData: getDataForCheckMore, + } = useGetDataForCheckMoreHooks(blockType)(oneStepRunParams.id, oneStepRunParams.data) + const [isRunAfterSingleRun, setIsRunAfterSingleRun] = useState(false) + + const { + id, + data, + } = oneStepRunParams + const oneStepRunRes = useOneStepRun({ + ...oneStepRunParams, + iteratorInputKey: blockType === BlockEnum.Iteration ? `${id}.input_selector` : '', + moreDataForCheckValid: getDataForCheckMore(), + isRunAfterSingleRun, + }) + + const { + appId, + hideSingleRun, + handleRun: doCallRunApi, + getInputVars, + toVarInputs, + varSelectorsToVarInputs, + runInputData, + runInputDataRef, + setRunInputData, + showSingleRun, + runResult, + iterationRunResult, + loopRunResult, + setNodeRunning, + checkValid, + } = oneStepRunRes + + const { + nodeInfo, + ...singleRunParams + } = useSingleRunFormParamsHooks(blockType)({ + id, + payload: data, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, + toVarInputs, + varSelectorsToVarInputs, + runResult, + iterationRunResult, + loopRunResult, + }) + + const toSubmitData = useCallback((data: Record) => { + if(!isIterationNode && !isLoopNode) + return data + + const allVarObject = singleRunParams?.allVarObject || {} + const formattedData: Record = {} + Object.keys(allVarObject).forEach((key) => { + const [varSectorStr, nodeId] = key.split(DELIMITER) + formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr] + }) + if(isIterationNode) { + const iteratorInputKey = `${id}.input_selector` + formattedData[iteratorInputKey] = data[iteratorInputKey] + } + return formattedData + }, [isIterationNode, isLoopNode, singleRunParams?.allVarObject, id]) + + const callRunApi = (data: Record, cb?: () => void) => { + handleSyncWorkflowDraft(true, true, { + onSuccess() { + doCallRunApi(toSubmitData(data)) + cb?.() + }, + }) + } + const workflowStore = useWorkflowStore() + const { setInitShowLastRunTab } = workflowStore.getState() + const initShowLastRunTab = useStore(s => s.initShowLastRunTab) + const [tabType, setTabType] = useState(initShowLastRunTab ? TabType.lastRun : TabType.settings) + useEffect(() => { + if(initShowLastRunTab) + setTabType(TabType.lastRun) + + setInitShowLastRunTab(false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initShowLastRunTab]) + const invalidLastRun = useInvalidLastRun(appId!, id) + + const handleRunWithParams = async (data: Record) => { + const { isValid } = checkValid() + if(!isValid) + return + setNodeRunning() + setIsRunAfterSingleRun(true) + setTabType(TabType.lastRun) + callRunApi(data, () => { + invalidLastRun() + }) + hideSingleRun() + } + + const handleTabClicked = useCallback((type: TabType) => { + setIsRunAfterSingleRun(false) + setTabType(type) + }, []) + + const getExistVarValuesInForms = (forms: FormProps[]) => { + if (!forms || forms.length === 0) + return [] + + const valuesArr = forms.map((form) => { + const values: Record = {} + form.inputs.forEach(({ variable, getVarValueFromDependent }) => { + const isGetValueFromDependent = getVarValueFromDependent || !variable.includes('.') + if(isGetValueFromDependent && !singleRunParams?.getDependentVar) + return + + const selector = isGetValueFromDependent ? (singleRunParams?.getDependentVar(variable) || []) : variable.slice(1, -1).split('.') + if(!selector || selector.length === 0) + return + const [nodeId, varName] = selector.slice(0, 2) + if(!isStartNode && nodeId === id) { // inner vars like loop vars + values[variable] = true + return + } + const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var + if (inspectVarValue) + values[variable] = true + }) + return values + }) + return valuesArr + } + + const isAllVarsHasValue = (vars?: ValueSelector[]) => { + if(!vars || vars.length === 0) + return true + return vars.every((varItem) => { + const [nodeId, varName] = varItem.slice(0, 2) + const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var + return inspectVarValue + }) + } + + const isSomeVarsHasValue = (vars?: ValueSelector[]) => { + if(!vars || vars.length === 0) + return true + return vars.some((varItem) => { + const [nodeId, varName] = varItem.slice(0, 2) + const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var + return inspectVarValue + }) + } + const getFilteredExistVarForms = (forms: FormProps[]) => { + if (!forms || forms.length === 0) + return [] + + const existVarValuesInForms = getExistVarValuesInForms(forms) + + const res = forms.map((form, i) => { + const existVarValuesInForm = existVarValuesInForms[i] + const newForm = { ...form } + const inputs = form.inputs.filter((input) => { + return !(input.variable in existVarValuesInForm) + }) + newForm.inputs = inputs + return newForm + }).filter(form => form.inputs.length > 0) + return res + } + + const checkAggregatorVarsSet = (vars: ValueSelector[][]) => { + if(!vars || vars.length === 0) + return true + // in each group, at last one set is ok + return vars.every((varItem) => { + return isSomeVarsHasValue(varItem) + }) + } + + const handleSingleRun = () => { + const { isValid } = checkValid() + if(!isValid) + return + const vars = singleRunParams?.getDependentVars?.() + // no need to input params + if (isAggregatorNode ? checkAggregatorVarsSet(vars) : isAllVarsHasValue(vars)) { + callRunApi({}, async () => { + setIsRunAfterSingleRun(true) + setNodeRunning() + invalidLastRun() + setTabType(TabType.lastRun) + }) + } + else { + showSingleRun() + } + } + + return { + ...oneStepRunRes, + tabType, + isRunAfterSingleRun, + setTabType: handleTabClicked, + singleRunParams, + nodeInfo, + setRunInputData, + handleSingleRun, + handleRunWithParams, + getExistVarValuesInForms, + getFilteredExistVarForms, + } +} + +export default useLastRun diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx new file mode 100644 index 000000000..09d7ed266 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx @@ -0,0 +1,34 @@ +'use client' +import TabHeader from '@/app/components/base/tab-header' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +export enum TabType { + settings = 'settings', + lastRun = 'lastRun', +} + +type Props = { + value: TabType, + onChange: (value: TabType) => void +} + +const Tab: FC = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + return ( + + ) +} +export default React.memo(Tab) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index f23af5812..769804a20 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -13,7 +13,7 @@ import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/a import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types' import { useStore as useAppStore } from '@/app/components/app/store' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import { getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow' +import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow' import Toast from '@/app/components/base/toast' import LLMDefault from '@/app/components/workflow/nodes/llm/default' import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default' @@ -32,7 +32,7 @@ import LoopDefault from '@/app/components/workflow/nodes/loop/default' import { ssePost } from '@/service/base' import { noop } from 'lodash-es' import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants' -import type { NodeTracing } from '@/types/workflow' +import type { NodeRunResult, NodeTracing } from '@/types/workflow' const { checkValid: checkLLMValid } = LLMDefault const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault const { checkValid: checkIfElseValid } = IfElseDefault @@ -47,7 +47,11 @@ const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault const { checkValid: checkIterationValid } = IterationDefault const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault const { checkValid: checkLoopValid } = LoopDefault - +import { + useStoreApi, +} from 'reactflow' +import { useInvalidLastRun } from '@/service/use-workflow' +import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud' // eslint-disable-next-line ts/no-unsafe-function-type const checkValidFns: Record = { [BlockEnum.LLM]: checkLLMValid, @@ -66,13 +70,15 @@ const checkValidFns: Record = { [BlockEnum.Loop]: checkLoopValid, } as any -type Params = { +export type Params = { id: string data: CommonNodeType defaultRunInputData: Record moreDataForCheckValid?: any iteratorInputKey?: string loopInputKey?: string + isRunAfterSingleRun: boolean + isPaused: boolean } const varTypeToInputVarType = (type: VarType, { @@ -105,6 +111,8 @@ const useOneStepRun = ({ moreDataForCheckValid, iteratorInputKey, loopInputKey, + isRunAfterSingleRun, + isPaused, }: Params) => { const { t } = useTranslation() const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any @@ -112,6 +120,7 @@ const useOneStepRun = ({ const isChatMode = useIsChatMode() const isIteration = data.type === BlockEnum.Iteration const isLoop = data.type === BlockEnum.Loop + const isStartNode = data.type === BlockEnum.Start const availableNodes = getBeforeNodesInSameBranch(id) const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id) @@ -143,6 +152,7 @@ const useOneStepRun = ({ } const checkValid = checkValidFns[data.type] + const appId = useAppStore.getState().appDetail?.id const [runInputData, setRunInputData] = useState>(defaultRunInputData || {}) const runInputDataRef = useRef(runInputData) @@ -150,11 +160,82 @@ const useOneStepRun = ({ runInputDataRef.current = data setRunInputData(data) }, []) - const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey].length : 0 - const loopTimes = loopInputKey ? runInputData[loopInputKey].length : 0 - const [runResult, setRunResult] = useState(null) + const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey]?.length : 0 + const loopTimes = loopInputKey ? runInputData[loopInputKey]?.length : 0 + + const store = useStoreApi() + const workflowStore = useWorkflowStore() + const { + setShowSingleRunPanel, + } = workflowStore.getState() + const invalidLastRun = useInvalidLastRun(appId!, id) + const [runResult, doSetRunResult] = useState(null) + const { + appendNodeInspectVars, + invalidateSysVarValues, + invalidateConversationVarValues, + } = useInspectVarsCrud() + const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart + const isPausedRef = useRef(isPaused) + useEffect(() => { + isPausedRef.current = isPaused + }, [isPaused]) + + const setRunResult = useCallback(async (data: NodeRunResult | null) => { + const isPaused = isPausedRef.current + + // The backend don't support pause the single run, so the frontend handle the pause state. + if(isPaused) + return + + const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded + if(!canRunLastRun) { + doSetRunResult(data) + return + } + + // run fail may also update the inspect vars when the node set the error default output. + const vars = await fetchNodeInspectVars(appId!, id) + const { getNodes } = store.getState() + const nodes = getNodes() + appendNodeInspectVars(id, vars, nodes) + if(data?.status === NodeRunningStatus.Succeeded) { + invalidLastRun() + if(isStartNode) + invalidateSysVarValues() + invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh. + } + }, [isRunAfterSingleRun, runningStatus, appId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues]) const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate() + const setNodeRunning = () => { + handleNodeDataUpdate({ + id, + data: { + ...data, + _singleRunningStatus: NodeRunningStatus.Running, + }, + }) + } + const checkValidWrap = () => { + if(!checkValid) + return { isValid: true, errorMessage: '' } + const res = checkValid(data, t, moreDataForCheckValid) + if(!res.isValid) { + handleNodeDataUpdate({ + id, + data: { + ...data, + _isSingleRun: false, + }, + }) + Toast.notify({ + type: 'error', + message: res.errorMessage, + }) + } + return res + } const [canShowSingleRun, setCanShowSingleRun] = useState(false) const isShowSingleRun = data._isSingleRun && canShowSingleRun const [iterationRunResult, setIterationRunResult] = useState([]) @@ -167,29 +248,15 @@ const useOneStepRun = ({ } if (data._isSingleRun) { - const { isValid, errorMessage } = checkValid(data, t, moreDataForCheckValid) + const { isValid } = checkValidWrap() setCanShowSingleRun(isValid) - if (!isValid) { - handleNodeDataUpdate({ - id, - data: { - ...data, - _isSingleRun: false, - }, - }) - Toast.notify({ - type: 'error', - message: errorMessage, - }) - } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data._isSingleRun]) - const workflowStore = useWorkflowStore() useEffect(() => { - workflowStore.getState().setShowSingleRunPanel(!!isShowSingleRun) - }, [isShowSingleRun, workflowStore]) + setShowSingleRunPanel(!!isShowSingleRun) + }, [isShowSingleRun, setShowSingleRunPanel]) const hideSingleRun = () => { handleNodeDataUpdate({ @@ -209,7 +276,6 @@ const useOneStepRun = ({ }, }) } - const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed const handleRun = async (submitData: Record) => { @@ -217,13 +283,29 @@ const useOneStepRun = ({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Running, }, }) let res: any + let hasError = false try { if (!isIteration && !isLoop) { - res = await singleNodeRun(appId!, id, { inputs: submitData }) as any + const isStartNode = data.type === BlockEnum.Start + const postData: Record = {} + if(isStartNode) { + const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData + if(isChatMode) + postData.conversation_id = '' + + postData.inputs = inputs + postData.query = query + postData.files = files || [] + } + else { + postData.inputs = submitData + } + res = await singleNodeRun(appId!, id, postData) as any } else if (isIteration) { setIterationRunResult([]) @@ -235,10 +317,13 @@ const useOneStepRun = ({ { onWorkflowStarted: noop, onWorkflowFinished: (params) => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Succeeded, }, }) @@ -311,10 +396,13 @@ const useOneStepRun = ({ setIterationRunResult(newIterationRunResult) }, onError: () => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Failed, }, }) @@ -332,10 +420,13 @@ const useOneStepRun = ({ { onWorkflowStarted: noop, onWorkflowFinished: (params) => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Succeeded, }, }) @@ -409,10 +500,13 @@ const useOneStepRun = ({ setLoopRunResult(newLoopRunResult) }, onError: () => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Failed, }, }) @@ -425,11 +519,16 @@ const useOneStepRun = ({ } catch (e: any) { console.error(e) + hasError = true + invalidLastRun() if (!isIteration && !isLoop) { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Failed, }, }) @@ -437,7 +536,7 @@ const useOneStepRun = ({ } } finally { - if (!isIteration && !isLoop) { + if (!isPausedRef.current && !isIteration && !isLoop && res) { setRunResult({ ...res, total_tokens: res.execution_metadata?.total_tokens || 0, @@ -445,11 +544,17 @@ const useOneStepRun = ({ }) } } - if (!isIteration && !isLoop) { + if(isPausedRef.current) + return + + if (!isIteration && !isLoop && !hasError) { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Succeeded, }, }) @@ -521,11 +626,19 @@ const useOneStepRun = ({ return varInputs } + const varSelectorsToVarInputs = (valueSelectors: ValueSelector[] | string[]): InputVar[] => { + return valueSelectors.filter(item => !!item).map((item) => { + return getInputVars([`{{#${typeof item === 'string' ? item : item.join('.')}#}}`])[0] + }) + } + return { + appId, isShowSingleRun, hideSingleRun, showSingleRun, toVarInputs, + varSelectorsToVarInputs, getInputVars, runningStatus, isCompleted, @@ -537,6 +650,8 @@ const useOneStepRun = ({ runResult, iterationRunResult, loopRunResult, + setNodeRunning, + checkValid: checkValidWrap, } } diff --git a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts index 839cd1402..515f2c365 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts @@ -1,6 +1,6 @@ -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' +import { useBoolean, useDebounceFn } from 'ahooks' import type { CodeNodeType, OutputVar, @@ -17,6 +17,7 @@ import { } from '@/app/components/workflow/hooks' import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' import { getDefaultValue } from '@/app/components/workflow/nodes/_base/components/error-handle/utils' +import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud' type Params = { id: string @@ -34,8 +35,27 @@ function useOutputVarList({ outputKeyOrders = [], onOutputKeyOrdersChange, }: Params) { + const { + renameInspectVarName, + deleteInspectVar, + nodesWithInspectVars, + } = useInspectVarsCrud() + const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow() + // record the first old name value + const oldNameRecord = useRef>({}) + + const { + run: renameInspectNameWithDebounce, + } = useDebounceFn( + (id: string, newName: string) => { + const oldName = oldNameRecord.current[id] + renameInspectVarName(id, oldName, newName) + delete oldNameRecord.current[id] + }, + { wait: 500 }, + ) const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => { const newInputs = produce(inputs, (draft: any) => { draft[varKey] = newVars @@ -52,9 +72,20 @@ function useOutputVarList({ onOutputKeyOrdersChange(newOutputKeyOrders) } - if (newKey) + if (newKey) { handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey]) - }, [inputs, setInputs, handleOutVarRenameChange, id, outputKeyOrders, varKey, onOutputKeyOrdersChange]) + if(!(id in oldNameRecord.current)) + oldNameRecord.current[id] = outputKeyOrders[changedIndex!] + renameInspectNameWithDebounce(id, newKey) + } + else if (changedIndex === undefined) { + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === Object.keys(newVars)[0] + })?.id + if(varId) + deleteInspectVar(id, varId) + } + }, [inputs, setInputs, varKey, outputKeyOrders, onOutputKeyOrdersChange, handleOutVarRenameChange, id, renameInspectNameWithDebounce, nodesWithInspectVars, deleteInspectVar]) const generateNewKey = useCallback(() => { let keyIndex = Object.keys((inputs as any)[varKey]).length + 1 @@ -86,9 +117,14 @@ function useOutputVarList({ }] = useBoolean(false) const [removedVar, setRemovedVar] = useState([]) const removeVarInNode = useCallback(() => { + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === removedVar[1] + })?.id + if(varId) + deleteInspectVar(id, varId) removeUsedVarInNodes(removedVar) hideRemoveVarConfirm() - }, [hideRemoveVarConfirm, removeUsedVarInNodes, removedVar]) + }, [deleteInspectVar, hideRemoveVarConfirm, id, nodesWithInspectVars, removeUsedVarInNodes, removedVar]) const handleRemoveVariable = useCallback((index: number) => { const key = outputKeyOrders[index] @@ -106,7 +142,12 @@ function useOutputVarList({ }) setInputs(newInputs) onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index)) - }, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, showRemoveVarConfirm, varKey]) + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === key + })?.id + if(varId) + deleteInspectVar(id, varId) + }, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, nodesWithInspectVars, deleteInspectVar, showRemoveVarConfirm, varKey]) return { handleVarsChange, diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 527b2f094..27d6adc62 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -44,6 +44,7 @@ import AddVariablePopupWithPosition from './components/add-variable-popup-with-p import cn from '@/utils/classnames' import BlockIcon from '@/app/components/workflow/block-icon' import Tooltip from '@/app/components/base/tooltip' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' type BaseNodeProps = { children: ReactElement @@ -89,6 +90,9 @@ const BaseNode: FC = ({ } }, [data.isInLoop, data.selected, id, handleNodeLoopChildSizeChange]) + const { hasNodeInspectVars } = useInspectVarsCrud() + const isLoading = data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running + const hasVarValue = hasNodeInspectVars(id) const showSelectedBorder = data.selected || data._isBundled || data._isEntering const { showRunningBorder, @@ -98,11 +102,11 @@ const BaseNode: FC = ({ } = useMemo(() => { return { showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder, - showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder, + showSuccessBorder: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && !showSelectedBorder, showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder, showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder, } - }, [data._runningStatus, showSelectedBorder]) + }, [data._runningStatus, hasVarValue, showSelectedBorder]) const LoopIndex = useMemo(() => { let text = '' @@ -260,12 +264,12 @@ const BaseNode: FC = ({ data.type === BlockEnum.Loop && data._loopIndex && LoopIndex } { - (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && ( + isLoading && ( ) } { - data._runningStatus === NodeRunningStatus.Succeeded && ( + (!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)) && ( ) } diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx deleted file mode 100644 index 49c61b341..000000000 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import type { - FC, - ReactNode, -} from 'react' -import { - cloneElement, - memo, - useCallback, -} from 'react' -import { - RiCloseLine, - RiPlayLargeLine, -} from '@remixicon/react' -import { useShallow } from 'zustand/react/shallow' -import { useTranslation } from 'react-i18next' -import NextStep from './components/next-step' -import PanelOperator from './components/panel-operator' -import HelpLink from './components/help-link' -import NodePosition from './components/node-position' -import { - DescriptionInput, - TitleInput, -} from './components/title-description-input' -import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel' -import RetryOnPanel from './components/retry/retry-on-panel' -import { useResizePanel } from './hooks/use-resize-panel' -import cn from '@/utils/classnames' -import BlockIcon from '@/app/components/workflow/block-icon' -import Split from '@/app/components/workflow/nodes/_base/components/split' -import { - WorkflowHistoryEvent, - useAvailableBlocks, - useNodeDataUpdate, - useNodesInteractions, - useNodesReadOnly, - useNodesSyncDraft, - useToolIcon, - useWorkflow, - useWorkflowHistory, -} from '@/app/components/workflow/hooks' -import { - canRunBySingle, - hasErrorHandleNode, - hasRetryNode, -} from '@/app/components/workflow/utils' -import Tooltip from '@/app/components/base/tooltip' -import type { Node } from '@/app/components/workflow/types' -import { useStore as useAppStore } from '@/app/components/app/store' -import { useStore } from '@/app/components/workflow/store' - -type BasePanelProps = { - children: ReactNode -} & Node - -const BasePanel: FC = ({ - id, - data, - children, - position, - width, - height, -}) => { - const { t } = useTranslation() - const { showMessageLogModal } = useAppStore(useShallow(state => ({ - showMessageLogModal: state.showMessageLogModal, - }))) - const showSingleRunPanel = useStore(s => s.showSingleRunPanel) - const panelWidth = localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420 - const { - setPanelWidth, - } = useWorkflow() - const { handleNodeSelect } = useNodesInteractions() - const { handleSyncWorkflowDraft } = useNodesSyncDraft() - const { nodesReadOnly } = useNodesReadOnly() - const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop) - const toolIcon = useToolIcon(data) - - const handleResize = useCallback((width: number) => { - setPanelWidth(width) - }, [setPanelWidth]) - - const { - triggerRef, - containerRef, - } = useResizePanel({ - direction: 'horizontal', - triggerDirection: 'left', - minWidth: 420, - maxWidth: 720, - onResize: handleResize, - }) - - const { saveStateToHistory } = useWorkflowHistory() - - const { - handleNodeDataUpdate, - handleNodeDataUpdateWithSyncDraft, - } = useNodeDataUpdate() - - const handleTitleBlur = useCallback((title: string) => { - handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) - saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) - }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) - const handleDescriptionChange = useCallback((desc: string) => { - handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) - saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) - }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) - - return ( -
-
-
-
-
-
-
- - -
- { - canRunBySingle(data.type) && !nodesReadOnly && ( - -
{ - handleNodeDataUpdate({ id, data: { _isSingleRun: true } }) - handleSyncWorkflowDraft(true) - }} - > - -
-
- ) - } - - - -
-
handleNodeSelect(id, true)} - > - -
-
-
-
- -
-
-
- {cloneElement(children as any, { id, data })} -
- - { - hasRetryNode(data.type) && ( - - ) - } - { - hasErrorHandleNode(data.type) && ( - - ) - } - { - !!availableNextBlocks.length && ( -
-
- {t('workflow.panel.nextStep').toLocaleUpperCase()} -
-
- {t('workflow.panel.addNextStep')} -
- -
- ) - } -
-
- ) -} - -export default memo(BasePanel) diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index f92e92dbc..391383031 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { memo, useMemo } from 'react' +import { memo } from 'react' import type { NodePanelProps } from '../../types' import { AgentFeature, type AgentNodeType } from './types' import Field from '../_base/components/field' @@ -9,16 +9,10 @@ import { useTranslation } from 'react-i18next' import OutputVars, { VarItem } from '../_base/components/output-vars' import type { StrategyParamItem } from '@/app/components/plugins/types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' -import formatTracing from '@/app/components/workflow/run/utils/format-log' -import { useLogs } from '@/app/components/workflow/run/hooks' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import { toType } from '@/app/components/tools/utils/to-form-schema' import { useStore } from '../../store' import Split from '../_base/components/split' import MemoryConfig from '../_base/components/memory-config' - const i18nPrefix = 'workflow.nodes.agent' export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema { @@ -42,41 +36,10 @@ const AgentPanel: FC> = (props) => { availableNodesWithParent, availableVars, readOnly, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - runInputData, - setRunInputData, - varInputs, outputSchema, handleMemoryChange, } = useConfig(props.id, props.data) const { t } = useTranslation() - const nodeInfo = useMemo(() => { - if (!runResult) - return - return formatTracing([runResult], t)[0] - }, [runResult, t]) - const logsParams = useLogs() - const singleRunForms = (() => { - const forms: FormProps[] = [] - - if (varInputs.length > 0) { - forms.push( - { - label: t(`${i18nPrefix}.singleRun.variable`)!, - inputs: varInputs, - values: runInputData, - onChange: setRunInputData, - }, - ) - } - - return forms - })() const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey) @@ -154,21 +117,6 @@ const AgentPanel: FC> = (props) => { ))}
- { - isShowSingleRun && ( - } - /> - ) - }
} diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 8196caa3f..c3e07e4e6 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -1,7 +1,6 @@ import { useStrategyProviderDetail } from '@/service/use-strategy' import useNodeCrud from '../_base/hooks/use-node-crud' import useVarList from '../_base/hooks/use-var-list' -import useOneStepRun from '../_base/hooks/use-one-step-run' import type { AgentNodeType } from './types' import { useIsChatMode, @@ -131,35 +130,6 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) // single run - const { - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - getInputVars, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: {}, - }) - const allVarStrArr = (() => { - const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => { - return formData[item.name] - }) || [] - - return arr - })() - const varInputs = (() => { - const vars = getInputVars(allVarStrArr) - - return vars - })() const outputSchema = useMemo(() => { const res: any[] = [] @@ -199,18 +169,6 @@ const useConfig = (id: string, payload: AgentNodeType) => { pluginDetail: pluginDetail.data?.plugins.at(0), availableVars, availableNodesWithParent, - - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - varInputs, outputSchema, handleMemoryChange, isChatMode, diff --git a/web/app/components/workflow/nodes/agent/use-single-run-form-params.ts b/web/app/components/workflow/nodes/agent/use-single-run-form-params.ts new file mode 100644 index 000000000..5ddc24b2f --- /dev/null +++ b/web/app/components/workflow/nodes/agent/use-single-run-form-params.ts @@ -0,0 +1,90 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { AgentNodeType } from './types' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import { useStrategyInfo } from './use-config' +import type { NodeTracing } from '@/types/workflow' +import formatTracing from '@/app/components/workflow/run/utils/format-log' + +type Params = { + id: string, + payload: AgentNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] + runResult: NodeTracing +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + getInputVars, + setRunInputData, + runResult, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud(id, payload) + + const formData = useMemo(() => { + return Object.fromEntries( + Object.entries(inputs.agent_parameters || {}).map(([key, value]) => { + return [key, value.value] + }), + ) + }, [inputs.agent_parameters]) + + const { + strategy: currentStrategy, + } = useStrategyInfo( + inputs.agent_strategy_provider_name, + inputs.agent_strategy_name, + ) + + const allVarStrArr = (() => { + const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => { + return formData[item.name] + }) || [] + return arr + })() + + const varInputs = getInputVars?.(allVarStrArr) + + const forms = useMemo(() => { + const forms: FormProps[] = [] + + if (varInputs!.length > 0) { + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: varInputs!, + values: runInputData, + onChange: setRunInputData, + }, + ) + } + return forms + }, [runInputData, setRunInputData, t, varInputs]) + + const nodeInfo = useMemo(() => { + if (!runResult) + return + return formatTracing([runResult], t)[0] + }, [runResult, t]) + + const getDependentVars = () => { + return varInputs.map(item => item.variable.slice(1, -1).split('.')) + } + + return { + forms, + nodeInfo, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index f34a1435a..b19d5903a 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -52,6 +52,7 @@ const VarList: FC = ({ const newList = produce(list, (draft) => { draft[index].variable_selector = value as ValueSelector draft[index].operation = WriteMode.overwrite + draft[index].input_type = AssignerNodeInputType.variable draft[index].value = undefined }) onChange(newList, value as ValueSelector) diff --git a/web/app/components/workflow/nodes/assigner/types.ts b/web/app/components/workflow/nodes/assigner/types.ts index 85d2b2850..22f37bb7c 100644 --- a/web/app/components/workflow/nodes/assigner/types.ts +++ b/web/app/components/workflow/nodes/assigner/types.ts @@ -30,3 +30,5 @@ export type AssignerNodeType = CommonNodeType & { version?: '1' | '2' items: AssignerNodeOperation[] } + +export const writeModeTypesNum = [WriteMode.increment, WriteMode.decrement, WriteMode.multiply, WriteMode.divide] diff --git a/web/app/components/workflow/nodes/assigner/use-config.ts b/web/app/components/workflow/nodes/assigner/use-config.ts index cbd547548..c42dd67b3 100644 --- a/web/app/components/workflow/nodes/assigner/use-config.ts +++ b/web/app/components/workflow/nodes/assigner/use-config.ts @@ -5,6 +5,7 @@ import { VarType } from '../../types' import type { ValueSelector, Var } from '../../types' import { WriteMode } from './types' import type { AssignerNodeOperation, AssignerNodeType } from './types' +import { writeModeTypesNum } from './types' import { useGetAvailableVars } from './hooks' import { convertV1ToV2 } from './utils' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' @@ -71,7 +72,6 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => { const writeModeTypesArr = [WriteMode.overwrite, WriteMode.clear, WriteMode.append, WriteMode.extend, WriteMode.removeFirst, WriteMode.removeLast] const writeModeTypes = [WriteMode.overwrite, WriteMode.clear, WriteMode.set] - const writeModeTypesNum = [WriteMode.increment, WriteMode.decrement, WriteMode.multiply, WriteMode.divide] const getToAssignedVarType = useCallback((assignedVarType: VarType, write_mode: WriteMode) => { if (write_mode === WriteMode.overwrite || write_mode === WriteMode.increment || write_mode === WriteMode.decrement diff --git a/web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts b/web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts new file mode 100644 index 000000000..c6d483a3e --- /dev/null +++ b/web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts @@ -0,0 +1,55 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { type AssignerNodeType, WriteMode } from './types' +import { writeModeTypesNum } from './types' + +type Params = { + id: string, + payload: AssignerNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + setRunInputData, + varSelectorsToVarInputs, +}: Params) => { + const { inputs } = useNodeCrud(id, payload) + + const vars = inputs.items.filter((item) => { + return item.operation !== WriteMode.clear && item.operation !== WriteMode.set + && item.operation !== WriteMode.removeFirst && item.operation !== WriteMode.removeLast + && !writeModeTypesNum.includes(item.operation) + }).map(item => item.value as ValueSelector) + + const forms = useMemo(() => { + const varInputs = varSelectorsToVarInputs(vars) + + return [ + { + inputs: varInputs, + values: runInputData, + onChange: setRunInputData, + }, + ] + }, [runInputData, setRunInputData, varSelectorsToVarInputs, vars]) + + const getDependentVars = () => { + return vars + } + + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/code/panel.tsx b/web/app/components/workflow/nodes/code/panel.tsx index a0b7535f8..05d6cd795 100644 --- a/web/app/components/workflow/nodes/code/panel.tsx +++ b/web/app/components/workflow/nodes/code/panel.tsx @@ -14,8 +14,6 @@ import Split from '@/app/components/workflow/nodes/_base/components/split' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector' import type { NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.code' const codeLanguages = [ @@ -50,16 +48,6 @@ const Panel: FC> = ({ isShowRemoveVarConfirm, hideRemoveVarConfirm, onRemoveVarConfirm, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - varInputs, - inputVarValues, - setInputVarValues, } = useConfig(id, data) const handleGeneratedCode = (value: string) => { @@ -128,25 +116,6 @@ const Panel: FC> = ({ />
- { - isShowSingleRun && ( - } - /> - ) - } { }) syncOutputKeyOrders(defaultConfig.outputs) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig]) const handleCodeChange = useCallback((code: string) => { @@ -104,38 +103,6 @@ const useConfig = (id: string, payload: CodeNodeType) => { return [VarType.string, VarType.number, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.file, VarType.arrayFile].includes(varPayload.type) }, []) - // single run - const { - isShowSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - isCompleted, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: {}, - }) - - const varInputs = toVarInputs(inputs.variables) - - const inputVarValues = (() => { - const vars: Record = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record) => { - setRunInputData(newPayload) - }, [setRunInputData]) const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => { const newInputs = produce(inputs, (draft) => { draft.code = code @@ -160,17 +127,6 @@ const useConfig = (id: string, payload: CodeNodeType) => { isShowRemoveVarConfirm, hideRemoveVarConfirm, onRemoveVarConfirm, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - isCompleted, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, handleCodeAndVarsChange, } } diff --git a/web/app/components/workflow/nodes/code/use-single-run-form-params.ts b/web/app/components/workflow/nodes/code/use-single-run-form-params.ts new file mode 100644 index 000000000..9714e55ff --- /dev/null +++ b/web/app/components/workflow/nodes/code/use-single-run-form-params.ts @@ -0,0 +1,65 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { CodeNodeType } from './types' + +type Params = { + id: string, + payload: CodeNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + toVarInputs, + setRunInputData, +}: Params) => { + const { inputs } = useNodeCrud(id, payload) + + const varInputs = toVarInputs(inputs.variables) + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + }, [inputVarValues, setInputVarValues, varInputs]) + + const getDependentVars = () => { + return payload.variables.map(v => v.value_selector) + } + + const getDependentVar = (variable: string) => { + const varItem = payload.variables.find(v => v.variable === variable) + if (varItem) + return varItem.value_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/document-extractor/panel.tsx b/web/app/components/workflow/nodes/document-extractor/panel.tsx index 5ed142577..a91608c71 100644 --- a/web/app/components/workflow/nodes/document-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/document-extractor/panel.tsx @@ -11,11 +11,9 @@ import useConfig from './use-config' import type { DocExtractorNodeType } from './types' import { fetchSupportFileTypes } from '@/service/datasets' import Field from '@/app/components/workflow/nodes/_base/components/field' -import { BlockEnum, InputVarType, type NodePanelProps } from '@/app/components/workflow/types' +import { BlockEnum, type NodePanelProps } from '@/app/components/workflow/types' import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n/language' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.docExtractor' @@ -48,15 +46,6 @@ const Panel: FC> = ({ inputs, handleVarChanges, filterVar, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - files, - setFiles, } = useConfig(id, data) return ( @@ -93,30 +82,6 @@ const Panel: FC> = ({ />
- { - isShowSingleRun && ( - setFiles(keyValue.files), - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={} - /> - ) - }
) } diff --git a/web/app/components/workflow/nodes/document-extractor/use-config.ts b/web/app/components/workflow/nodes/document-extractor/use-config.ts index 8ceb15387..43f3e71fa 100644 --- a/web/app/components/workflow/nodes/document-extractor/use-config.ts +++ b/web/app/components/workflow/nodes/document-extractor/use-config.ts @@ -1,12 +1,10 @@ import { useCallback, useMemo } from 'react' import produce from 'immer' import { useStoreApi } from 'reactflow' - import type { ValueSelector, Var } from '../../types' -import { InputVarType, VarType } from '../../types' +import { VarType } from '../../types' import type { DocExtractorNodeType } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useIsChatMode, useNodesReadOnly, @@ -58,53 +56,11 @@ const useConfig = (id: string, payload: DocExtractorNodeType) => { setInputs(newInputs) }, [getType, inputs, setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - runningStatus, - isCompleted, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: { files: [] }, - }) - const varInputs = [{ - label: inputs.title, - variable: 'files', - type: InputVarType.multiFiles, - required: true, - }] - - const files = runInputData.files - const setFiles = useCallback((newFiles: []) => { - setRunInputData({ - ...runInputData, - files: newFiles, - }) - }, [runInputData, setRunInputData]) - return { readOnly, inputs, filterVar, handleVarChanges, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - isCompleted, - handleRun, - handleStop, - varInputs, - files, - setFiles, - runResult, } } diff --git a/web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts b/web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts new file mode 100644 index 000000000..3b249cd21 --- /dev/null +++ b/web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts @@ -0,0 +1,64 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import type { DocExtractorNodeType } from './types' +import { useTranslation } from 'react-i18next' +import { InputVarType } from '@/app/components/workflow/types' + +const i18nPrefix = 'workflow.nodes.docExtractor' + +type Params = { + id: string, + payload: DocExtractorNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const files = runInputData.files + const setFiles = useCallback((newFiles: []) => { + setRunInputData({ + ...runInputData, + files: newFiles, + }) + }, [runInputData, setRunInputData]) + + const forms = useMemo(() => { + return [ + { + inputs: [{ + label: t(`${i18nPrefix}.inputVar`)!, + variable: 'files', + type: InputVarType.multiFiles, + required: true, + }], + values: { files }, + onChange: (keyValue: Record) => setFiles(keyValue.files), + }, + ] + }, [files, setFiles, t]) + + const getDependentVars = () => { + return [payload.variable_selector] + } + + const getDependentVar = (variable: string) => { + if(variable === 'files') + return payload.variable_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/http/panel.tsx b/web/app/components/workflow/nodes/http/panel.tsx index 60f3de81c..9a07c0ad6 100644 --- a/web/app/components/workflow/nodes/http/panel.tsx +++ b/web/app/components/workflow/nodes/http/panel.tsx @@ -16,8 +16,6 @@ import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/compo import { Settings01 } from '@/app/components/base/icons/src/vender/line/general' import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files' import type { NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.http' @@ -45,16 +43,6 @@ const Panel: FC> = ({ hideAuthorization, setAuthorization, setTimeout, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, isShowCurlPanel, showCurlPanel, hideCurlPanel, @@ -180,24 +168,6 @@ const Panel: FC> = ({
- {isShowSingleRun && ( - } - /> - )} {(isShowCurlPanel && !readOnly) && ( { return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) }, []) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: {}, - }) - - const fileVarInputs = useMemo(() => { - if (!Array.isArray(inputs.body.data)) - return '' - - const res = inputs.body.data - .filter(item => item.file?.length) - .map(item => item.file ? `{{#${item.file.join('.')}#}}` : '') - .join(' ') - return res - }, [inputs.body.data]) - - const varInputs = getInputVars([ - inputs.url, - inputs.headers, - inputs.params, - typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data?.map(item => item.value).join(''), - fileVarInputs, - ]) - - const inputVarValues = (() => { - const vars: Record = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record) => { - setRunInputData(newPayload) - }, [setRunInputData]) - // curl import panel const [isShowCurlPanel, { setTrue: showCurlPanel, @@ -220,16 +170,6 @@ const useConfig = (id: string, payload: HttpNodeType) => { hideAuthorization, setAuthorization, setTimeout, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, // curl import isShowCurlPanel, showCurlPanel, diff --git a/web/app/components/workflow/nodes/http/use-single-run-form-params.ts b/web/app/components/workflow/nodes/http/use-single-run-form-params.ts new file mode 100644 index 000000000..c5d65634c --- /dev/null +++ b/web/app/components/workflow/nodes/http/use-single-run-form-params.ts @@ -0,0 +1,74 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { HttpNodeType } from './types' + +type Params = { + id: string, + payload: HttpNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + getInputVars, + setRunInputData, +}: Params) => { + const { inputs } = useNodeCrud(id, payload) + + const fileVarInputs = useMemo(() => { + if (!Array.isArray(inputs.body.data)) + return '' + + const res = inputs.body.data + .filter(item => item.file?.length) + .map(item => item.file ? `{{#${item.file.join('.')}#}}` : '') + .join(' ') + return res + }, [inputs.body.data]) + const varInputs = getInputVars([ + inputs.url, + inputs.headers, + inputs.params, + typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data?.map(item => item.value).join(''), + fileVarInputs, + ]) + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + }, [inputVarValues, setInputVarValues, varInputs]) + + const getDependentVars = () => { + return varInputs.map(item => item.variable.slice(1, -1).split('.')) + } + + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx index 9036e04d3..a2b3cb758 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx @@ -69,7 +69,7 @@ const ConditionOperator = ({ - +
{ options.map(option => ( diff --git a/web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts b/web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts new file mode 100644 index 000000000..f61f2846c --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts @@ -0,0 +1,166 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useCallback } from 'react' +import type { CaseItem, Condition, IfElseNodeType } from './types' + +type Params = { + id: string, + payload: IfElseNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, + getInputVars, + varSelectorsToVarInputs, +}: Params) => { + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const getVarSelectorsFromCase = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarSelectorsFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getVarSelectorsFromCondition = (condition: Condition) => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition)) + return vars + } + + const getInputVarsFromCase = (caseItem: CaseItem): InputVar[] => { + const vars: InputVar[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getInputVarsFromConditionValue(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getInputVarsFromConditionValue = (condition: Condition): InputVar[] => { + const vars: InputVar[] = [] + if (condition.value && typeof condition.value === 'string') { + const inputVars = getInputVars([condition.value]) + vars.push(...inputVars) + } + + if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getInputVarsFromCase(condition.sub_variable_condition)) + + return vars + } + + const forms = (() => { + const allInputs: ValueSelector[] = [] + const inputVarsFromValue: InputVar[] = [] + if (payload.cases && payload.cases.length) { + payload.cases.forEach((caseItem) => { + const caseVars = getVarSelectorsFromCase(caseItem) + allInputs.push(...caseVars) + inputVarsFromValue.push(...getInputVarsFromCase(caseItem)) + }) + } + + if (payload.conditions && payload.conditions.length) { + payload.conditions.forEach((condition) => { + const conditionVars = getVarSelectorsFromCondition(condition) + allInputs.push(...conditionVars) + inputVarsFromValue.push(...getInputVarsFromConditionValue(condition)) + }) + } + + const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue] + // remove duplicate inputs + const existVarsKey: Record = {} + const uniqueVarInputs: InputVar[] = [] + varInputs.forEach((input) => { + if(!input) + return + if (!existVarsKey[input.variable]) { + existVarsKey[input.variable] = true + uniqueVarInputs.push(input) + } + }) + return [ + { + inputs: uniqueVarInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + })() + + const getVarFromCaseItem = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + const getVarFromCondition = (condition: Condition): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if(condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarFromCaseItem(condition.sub_variable_condition)) + return vars + } + + const getDependentVars = () => { + const vars: ValueSelector[] = [] + if (payload.cases && payload.cases.length) { + payload.cases.forEach((caseItem) => { + const caseVars = getVarFromCaseItem(caseItem) + vars.push(...caseVars) + }) + } + + if (payload.conditions && payload.conditions.length) { + payload.conditions.forEach((condition) => { + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index bebc14041..d120ed8d3 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -10,7 +10,7 @@ import { PanelComponentMap, } from './constants' import BaseNode from './_base/node' -import BasePanel from './_base/panel' +import BasePanel from './_base/components/workflow-panel' const CustomNode = (props: NodeProps) => { const nodeData = props.data @@ -18,7 +18,7 @@ const CustomNode = (props: NodeProps) => { return ( <> - + diff --git a/web/app/components/workflow/nodes/iteration/panel.tsx b/web/app/components/workflow/nodes/iteration/panel.tsx index 1f29a0794..4b529f078 100644 --- a/web/app/components/workflow/nodes/iteration/panel.tsx +++ b/web/app/components/workflow/nodes/iteration/panel.tsx @@ -3,20 +3,15 @@ import React from 'react' import { useTranslation } from 'react-i18next' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import Split from '../_base/components/split' -import ResultPanel from '../../run/result-panel' import { MAX_ITERATION_PARALLEL_NUM, MIN_ITERATION_PARALLEL_NUM } from '../../constants' import type { IterationNodeType } from './types' import useConfig from './use-config' -import { ErrorHandleMode, InputVarType, type NodePanelProps } from '@/app/components/workflow/types' +import { ErrorHandleMode, type NodePanelProps } from '@/app/components/workflow/types' import Field from '@/app/components/workflow/nodes/_base/components/field' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import Switch from '@/app/components/base/switch' import Select from '@/app/components/base/select' import Slider from '@/app/components/base/slider' import Input from '@/app/components/base/input' -import formatTracing from '@/app/components/workflow/run/utils/format-log' - -import { useLogs } from '@/app/components/workflow/run/hooks' const i18nPrefix = 'workflow.nodes.iteration' @@ -47,27 +42,11 @@ const Panel: FC> = ({ childrenNodeVars, iterationChildrenNodes, handleOutputVarChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - inputVarValues, - setInputVarValues, - usedOutVars, - iterator, - setIterator, - iteratorInputKey, - iterationRunResult, changeParallel, changeErrorResponseMode, changeParallelNums, } = useConfig(id, data) - const nodeInfo = formatTracing(iterationRunResult, t)[0] - const logsParams = useLogs() - return (
@@ -137,38 +116,6 @@ const Panel: FC> = ({
*/} - {isShowSingleRun && ( - - } - /> - )}
) } diff --git a/web/app/components/workflow/nodes/loop/use-config.ts b/web/app/components/workflow/nodes/loop/use-config.ts index fbd350c22..965fe2b39 100644 --- a/web/app/components/workflow/nodes/loop/use-config.ts +++ b/web/app/components/workflow/nodes/loop/use-config.ts @@ -3,7 +3,6 @@ import { useRef, } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' import { v4 as uuid4 } from 'uuid' import { useIsChatMode, @@ -12,10 +11,9 @@ import { useWorkflow, } from '../../hooks' import { ValueType, VarType } from '../../types' -import type { ErrorHandleMode, ValueSelector, Var } from '../../types' +import type { ErrorHandleMode, Var } from '../../types' import useNodeCrud from '../_base/hooks/use-node-crud' -import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils' -import useOneStepRun from '../_base/hooks/use-one-step-run' +import { toNodeOutputVars } from '../_base/components/variable/utils' import { getOperators } from './utils' import { LogicalOperator } from './types' import type { HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, LoopNodeType } from './types' @@ -47,140 +45,12 @@ const useConfig = (id: string, payload: LoopNodeType) => { const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes] const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables) - // single run - const loopInputKey = `${id}.input_selector` - const { - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun: doHandleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - loopRunResult, - } = useOneStepRun({ - id, - data: inputs, - loopInputKey, - defaultRunInputData: { - [loopInputKey]: [''], - }, - }) - - const [isShowLoopDetail, { - setTrue: doShowLoopDetail, - setFalse: doHideLoopDetail, - }] = useBoolean(false) - - const hideLoopDetail = useCallback(() => { - hideSingleRun() - doHideLoopDetail() - }, [doHideLoopDetail, hideSingleRun]) - - const showLoopDetail = useCallback(() => { - doShowLoopDetail() - }, [doShowLoopDetail]) - - const backToSingleRun = useCallback(() => { - hideLoopDetail() - showSingleRun() - }, [hideLoopDetail, showSingleRun]) - const { getIsVarFileAttribute, } = useIsVarFileAttribute({ nodeId: id, }) - const { usedOutVars, allVarObject } = (() => { - const vars: ValueSelector[] = [] - const varObjs: Record = {} - const allVarObject: Record = {} - loopChildrenNodes.forEach((node) => { - const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0) - nodeVars.forEach((varSelector) => { - if (varSelector[0] === id) { // skip Loop node itself variable: item, index - return - } - const isInLoop = isNodeInLoop(varSelector[0]) - if (isInLoop) // not pass loop inner variable - return - - const varSectorStr = varSelector.join('.') - if (!varObjs[varSectorStr]) { - varObjs[varSectorStr] = true - vars.push(varSelector) - } - let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector) - if (typeof passToServerKeys === 'string') - passToServerKeys = [passToServerKeys] - - passToServerKeys.forEach((key: string, index: number) => { - allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = { - inSingleRunPassedKey: key, - } - }) - }) - }) - const res = toVarInputs(vars.map((item) => { - const varInfo = getNodeInfoById(canChooseVarNodes, item[0]) - return { - label: { - nodeType: varInfo?.data.type, - nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title - variable: isSystemVar(item) ? item.join('.') : item[item.length - 1], - }, - variable: `${item.join('.')}`, - value_selector: item, - } - })) - return { - usedOutVars: res, - allVarObject, - } - })() - - const handleRun = useCallback((data: Record) => { - const formattedData: Record = {} - Object.keys(allVarObject).forEach((key) => { - const [varSectorStr, nodeId] = key.split(DELIMITER) - formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr] - }) - formattedData[loopInputKey] = data[loopInputKey] - doHandleRun(formattedData) - }, [allVarObject, doHandleRun, loopInputKey]) - - const inputVarValues = (() => { - const vars: Record = {} - Object.keys(runInputData) - .filter(key => ![loopInputKey].includes(key)) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record) => { - const newVars = { - ...newPayload, - [loopInputKey]: runInputData[loopInputKey], - } - setRunInputData(newVars) - }, [loopInputKey, runInputData, setRunInputData]) - - const loop = runInputData[loopInputKey] - const setLoop = useCallback((newLoop: string[]) => { - setRunInputData({ - ...runInputData, - [loopInputKey]: newLoop, - }) - }, [loopInputKey, runInputData, setRunInputData]) - const changeErrorResponseMode = useCallback((item: { value: unknown }) => { const newInputs = produce(inputs, (draft) => { draft.error_handle_mode = item.value as ErrorHandleMode @@ -342,24 +212,6 @@ const useConfig = (id: string, payload: LoopNodeType) => { filterInputVar, childrenNodeVars, loopChildrenNodes, - isShowSingleRun, - showSingleRun, - hideSingleRun, - isShowLoopDetail, - showLoopDetail, - hideLoopDetail, - backToSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - inputVarValues, - setInputVarValues, - usedOutVars, - loop, - setLoop, - loopInputKey, - loopRunResult, handleAddCondition, handleRemoveCondition, handleUpdateCondition, diff --git a/web/app/components/workflow/nodes/loop/use-single-run-form-params.ts b/web/app/components/workflow/nodes/loop/use-single-run-form-params.ts new file mode 100644 index 000000000..394ab9b16 --- /dev/null +++ b/web/app/components/workflow/nodes/loop/use-single-run-form-params.ts @@ -0,0 +1,221 @@ +import type { NodeTracing } from '@/types/workflow' +import { useCallback, useMemo } from 'react' +import formatTracing from '@/app/components/workflow/run/utils/format-log' +import { useTranslation } from 'react-i18next' +import { useIsNodeInLoop, useWorkflow } from '../../hooks' +import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils' +import type { InputVar, ValueSelector, Variable } from '../../types' +import type { CaseItem, Condition, LoopNodeType } from './types' +import { ValueType } from '@/app/components/workflow/types' +import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config' + +type Params = { + id: string + payload: LoopNodeType + runInputData: Record + runResult: NodeTracing + loopRunResult: NodeTracing[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} + +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runResult, + loopRunResult, + setRunInputData, + toVarInputs, + varSelectorsToVarInputs, +}: Params) => { + const { t } = useTranslation() + + const { isNodeInLoop } = useIsNodeInLoop(id) + + const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow() + const loopChildrenNodes = getLoopNodeChildren(id) + const beforeNodes = getBeforeNodesInSameBranch(id) + const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes] + + const { usedOutVars, allVarObject } = (() => { + const vars: ValueSelector[] = [] + const varObjs: Record = {} + const allVarObject: Record = {} + loopChildrenNodes.forEach((node) => { + const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0) + nodeVars.forEach((varSelector) => { + if (varSelector[0] === id) { // skip loop node itself variable: item, index + return + } + const isInLoop = isNodeInLoop(varSelector[0]) + if (isInLoop) // not pass loop inner variable + return + + const varSectorStr = varSelector.join('.') + if (!varObjs[varSectorStr]) { + varObjs[varSectorStr] = true + vars.push(varSelector) + } + let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector) + if (typeof passToServerKeys === 'string') + passToServerKeys = [passToServerKeys] + + passToServerKeys.forEach((key: string, index: number) => { + allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = { + inSingleRunPassedKey: key, + } + }) + }) + }) + + const res = toVarInputs(vars.map((item) => { + const varInfo = getNodeInfoById(canChooseVarNodes, item[0]) + return { + label: { + nodeType: varInfo?.data.type, + nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title + variable: isSystemVar(item) ? item.join('.') : item[item.length - 1], + }, + variable: `${item.join('.')}`, + value_selector: item, + } + })) + return { + usedOutVars: res, + allVarObject, + } + })() + + const nodeInfo = useMemo(() => { + const formattedNodeInfo = formatTracing(loopRunResult, t)[0] + + if (runResult && formattedNodeInfo) { + return { + ...formattedNodeInfo, + execution_metadata: { + ...runResult.execution_metadata, + ...formattedNodeInfo.execution_metadata, + }, + } + } + + return formattedNodeInfo + }, [runResult, loopRunResult, t]) + + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const getVarSelectorsFromCase = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarSelectorsFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getVarSelectorsFromCondition = (condition: Condition) => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition)) + return vars + } + + const forms = (() => { + const allInputs: ValueSelector[] = [] + payload.break_conditions?.forEach((condition) => { + const vars = getVarSelectorsFromCondition(condition) + allInputs.push(...vars) + }) + + payload.loop_variables?.forEach((loopVariable) => { + if(loopVariable.value_type === ValueType.variable) + allInputs.push(loopVariable.value) + }) + const inputVarsFromValue: InputVar[] = [] + const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue] + + const existVarsKey: Record = {} + const uniqueVarInputs: InputVar[] = [] + varInputs.forEach((input) => { + if(!input) + return + if (!existVarsKey[input.variable]) { + existVarsKey[input.variable] = true + uniqueVarInputs.push(input) + } + }) + return [ + { + inputs: [...usedOutVars, ...uniqueVarInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + })() + + const getVarFromCaseItem = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getVarFromCondition = (condition: Condition): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if(condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarFromCaseItem(condition.sub_variable_condition)) + return vars + } + + const getDependentVars = () => { + const vars: ValueSelector[] = usedOutVars.map(item => item.variable.split('.')) + payload.break_conditions?.forEach((condition) => { + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + payload.loop_variables?.forEach((loopVariable) => { + if(loopVariable.value_type === ValueType.variable) + vars.push(loopVariable.value) + }) + const hasFilterLoopVars = vars.filter(item => item[0] !== id) + return hasFilterLoopVars + } + + return { + forms, + nodeInfo, + allVarObject, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/parameter-extractor/panel.tsx b/web/app/components/workflow/nodes/parameter-extractor/panel.tsx index d03f1d9ff..e86a2e376 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/panel.tsx @@ -4,9 +4,7 @@ import { useTranslation } from 'react-i18next' import MemoryConfig from '../_base/components/memory-config' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import Editor from '../_base/components/prompt/editor' -import ResultPanel from '../../run/result-panel' import ConfigVision from '../_base/components/config-vision' -import { findVariableWhenOnLLMVision } from '../utils' import useConfig from './use-config' import type { ParameterExtractorNodeType } from './types' import ExtractParameter from './components/extract-parameter/list' @@ -17,12 +15,10 @@ import Field from '@/app/components/workflow/nodes/_base/components/field' import Split from '@/app/components/workflow/nodes/_base/components/split' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types' +import type { NodePanelProps } from '@/app/components/workflow/types' import Tooltip from '@/app/components/base/tooltip' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import { VarType } from '@/app/components/workflow/types' import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' const i18nPrefix = 'workflow.nodes.parameterExtractor' const i18nCommonPrefix = 'workflow.common' @@ -53,63 +49,13 @@ const Panel: FC> = ({ handleReasoningModeChange, availableVars, availableNodesWithParent, - availableVisionVars, - inputVarValues, - varInputs, isVisionModel, handleVisionResolutionChange, handleVisionResolutionEnabledChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - setInputVarValues, - visionFiles, - setVisionFiles, } = useConfig(id, data) const model = inputs.model - const singleRunForms = (() => { - const forms: FormProps[] = [] - - forms.push( - { - label: t('workflow.nodes.llm.singleRun.variable')!, - inputs: [{ - label: t(`${i18nPrefix}.inputVar`)!, - variable: 'query', - type: InputVarType.paragraph, - required: true, - }, ...varInputs], - values: inputVarValues, - onChange: setInputVarValues, - }, - ) - - if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) { - const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVisionVars) - - forms.push( - { - label: t('workflow.nodes.llm.vision')!, - inputs: [{ - label: currentVariable?.variable as any, - variable: '#files#', - type: currentVariable?.formType as any, - required: false, - }], - values: { '#files#': visionFiles }, - onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), - }, - ) - } - - return forms - })() - return (
@@ -255,17 +201,6 @@ const Panel: FC> = ({
)} - {isShowSingleRun && ( - } - /> - )}
) } diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-config.ts b/web/app/components/workflow/nodes/parameter-extractor/use-config.ts index 045737b23..3fe42b60c 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/use-config.ts +++ b/web/app/components/workflow/nodes/parameter-extractor/use-config.ts @@ -8,7 +8,6 @@ import { useNodesReadOnly, useWorkflow, } from '../../hooks' -import useOneStepRun from '../_base/hooks/use-one-step-run' import useConfigVision from '../../hooks/use-config-vision' import type { Param, ParameterExtractorNodeType, ReasoningModeType } from './types' import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -17,8 +16,13 @@ import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-cr import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { supportFunctionCall } from '@/utils/tool-call' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: ParameterExtractorNodeType) => { + const { + deleteNodeInspectorVars, + renameInspectVarName, + } = useInspectVarsCrud() const { nodesReadOnly: readOnly } = useNodesReadOnly() const { handleOutVarRenameChange } = useWorkflow() const isChatMode = useIsChatMode() @@ -59,9 +63,14 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { }) setInputs(newInputs) - if (moreInfo && moreInfo?.type === ChangeType.changeVarName && moreInfo.payload) + if (moreInfo && moreInfo?.type === ChangeType.changeVarName && moreInfo.payload) { handleOutVarRenameChange(id, [id, moreInfo.payload.beforeKey], [id, moreInfo.payload.afterKey!]) - }, [handleOutVarRenameChange, id, inputs, setInputs]) + renameInspectVarName(id, moreInfo.payload.beforeKey, moreInfo.payload.afterKey!) + } + else { + deleteNodeInspectorVars(id) + } + }, [deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, renameInspectVarName, setInputs]) const addExtractParameter = useCallback((payload: Param) => { const newInputs = produce(inputs, (draft) => { @@ -70,7 +79,8 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { draft.parameters.push(payload) }) setInputs(newInputs) - }, [inputs, setInputs]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, id, inputs, setInputs]) // model const model = inputs.model || { @@ -145,7 +155,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { return setModelChanged(false) handleVisionConfigAfterModelChanged() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisionModel, modelChanged]) const { @@ -163,10 +173,6 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { return [VarType.number, VarType.string].includes(varPayload.type) }, []) - const filterVisionInputVar = useCallback((varPayload: Var) => { - return [VarType.file, VarType.arrayFile].includes(varPayload.type) - }, []) - const { availableVars, availableNodesWithParent, @@ -175,13 +181,6 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { filterVar: filterInputVar, }) - const { - availableVars: availableVisionVars, - } = useAvailableVarList(id, { - onlyLeafNodeVar: false, - filterVar: filterVisionInputVar, - }) - const handleCompletionParamsChange = useCallback((newParams: Record) => { const newInputs = produce(inputs, (draft) => { draft.model.completion_params = newParams @@ -223,49 +222,6 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { setInputs(newInputs) }, [inputs, setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - runInputDataRef, - setRunInputData, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: { - 'query': '', - '#files#': [], - }, - }) - - const varInputs = getInputVars([inputs.instruction]) - const inputVarValues = (() => { - const vars: Record = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record) => { - setRunInputData(newPayload) - }, [setRunInputData]) - - const visionFiles = runInputData['#files#'] - const setVisionFiles = useCallback((newFiles: any[]) => { - setRunInputData({ - ...runInputDataRef.current, - '#files#': newFiles, - }) - }, [runInputDataRef, setRunInputData]) - return { readOnly, handleInputVarChange, @@ -283,24 +239,12 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { hasSetBlockStatus, availableVars, availableNodesWithParent, - availableVisionVars, isSupportFunctionCall, handleReasoningModeChange, handleMemoryChange, - varInputs, - inputVarValues, isVisionModel, handleVisionResolutionEnabledChange, handleVisionResolutionChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - setInputVarValues, - visionFiles, - setVisionFiles, } } diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts new file mode 100644 index 000000000..178f9e3ed --- /dev/null +++ b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts @@ -0,0 +1,148 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { InputVar, Var, Variable } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' +import type { ParameterExtractorNodeType } from './types' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { useCallback } from 'react' +import useConfigVision from '../../hooks/use-config-vision' +import { noop } from 'lodash-es' +import { findVariableWhenOnLLMVision } from '../utils' +import useAvailableVarList from '../_base/hooks/use-available-var-list' + +const i18nPrefix = 'workflow.nodes.parameterExtractor' + +type Params = { + id: string, + payload: ParameterExtractorNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud(id, payload) + + const model = inputs.model + + const { + isVisionModel, + } = useConfigVision(model, { + payload: inputs.vision, + onChange: noop, + }) + + const visionFiles = runInputData['#files#'] + const setVisionFiles = useCallback((newFiles: any[]) => { + setRunInputData?.({ + ...runInputDataRef.current, + '#files#': newFiles, + }) + }, [runInputDataRef, setRunInputData]) + + const varInputs = getInputVars([inputs.instruction]) + + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .filter(key => !['#context#', '#files#'].includes(key)) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const setInputVarValues = useCallback((newPayload: Record) => { + const newVars = { + ...newPayload, + '#context#': runInputDataRef.current['#context#'], + '#files#': runInputDataRef.current['#files#'], + } + setRunInputData?.(newVars) + }, [runInputDataRef, setRunInputData]) + + const filterVisionInputVar = useCallback((varPayload: Var) => { + return [VarType.file, VarType.arrayFile].includes(varPayload.type) + }, []) + const { + availableVars: availableVisionVars, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterVisionInputVar, + }) + + const forms = (() => { + const forms: FormProps[] = [] + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: [{ + label: t(`${i18nPrefix}.inputVar`)!, + variable: 'query', + type: InputVarType.paragraph, + required: true, + }, ...varInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ) + + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const currentVariable = findVariableWhenOnLLMVision(payload.vision.configs.variable_selector, availableVisionVars) + + forms.push( + { + label: t('workflow.nodes.llm.vision')!, + inputs: [{ + label: currentVariable?.variable as any, + variable: '#files#', + type: currentVariable?.formType as any, + required: false, + }], + values: { '#files#': visionFiles }, + onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), + }, + ) + } + + return forms + })() + + const getDependentVars = () => { + const promptVars = varInputs.map(item => item.variable.slice(1, -1).split('.')) + const vars = [payload.query, ...promptVars] + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const visionVar = payload.vision.configs.variable_selector + vars.push(visionVar) + } + return vars + } + + const getDependentVar = (variable: string) => { + if(variable === 'query') + return payload.query + if(variable === '#files#') + return payload.vision.configs?.variable_selector + + return false + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/question-classifier/panel.tsx b/web/app/components/workflow/nodes/question-classifier/panel.tsx index d2e0fb060..8f6f5eb76 100644 --- a/web/app/components/workflow/nodes/question-classifier/panel.tsx +++ b/web/app/components/workflow/nodes/question-classifier/panel.tsx @@ -3,20 +3,16 @@ import React from 'react' import { useTranslation } from 'react-i18next' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import ConfigVision from '../_base/components/config-vision' -import { findVariableWhenOnLLMVision } from '../utils' import useConfig from './use-config' import ClassList from './components/class-list' import AdvancedSetting from './components/advanced-setting' import type { QuestionClassifierNodeType } from './types' import Field from '@/app/components/workflow/nodes/_base/components/field' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' -import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import type { NodePanelProps } from '@/app/components/workflow/types' import Split from '@/app/components/workflow/nodes/_base/components/split' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' const i18nPrefix = 'workflow.nodes.questionClassifiers' @@ -38,66 +34,16 @@ const Panel: FC> = ({ hasSetBlockStatus, availableVars, availableNodesWithParent, - availableVisionVars, handleInstructionChange, - inputVarValues, - varInputs, - setInputVarValues, handleMemoryChange, isVisionModel, handleVisionResolutionChange, handleVisionResolutionEnabledChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, filterVar, - visionFiles, - setVisionFiles, } = useConfig(id, data) const model = inputs.model - const singleRunForms = (() => { - const forms: FormProps[] = [] - - forms.push( - { - label: t('workflow.nodes.llm.singleRun.variable')!, - inputs: [{ - label: t(`${i18nPrefix}.inputVars`)!, - variable: 'query', - type: InputVarType.paragraph, - required: true, - }, ...varInputs], - values: inputVarValues, - onChange: setInputVarValues, - }, - ) - - if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) { - const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVisionVars) - - forms.push( - { - label: t('workflow.nodes.llm.vision')!, - inputs: [{ - label: currentVariable?.variable as any, - variable: '#files#', - type: currentVariable?.formType as any, - required: false, - }], - values: { '#files#': visionFiles }, - onChange: keyValue => setVisionFiles(keyValue['#files#']), - }, - ) - } - - return forms - })() - return (
@@ -186,17 +132,6 @@ const Panel: FC> = ({
- {isShowSingleRun && ( - } - /> - )}
) } diff --git a/web/app/components/workflow/nodes/question-classifier/use-config.ts b/web/app/components/workflow/nodes/question-classifier/use-config.ts index 7df8293b4..8eacf5b43 100644 --- a/web/app/components/workflow/nodes/question-classifier/use-config.ts +++ b/web/app/components/workflow/nodes/question-classifier/use-config.ts @@ -11,7 +11,6 @@ import useAvailableVarList from '../_base/hooks/use-available-var-list' import useConfigVision from '../../hooks/use-config-vision' import type { QuestionClassifierNodeType } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' @@ -87,7 +86,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { return setModelChanged(false) handleVisionConfigAfterModelChanged() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisionModel, modelChanged]) const handleQueryVarChange = useCallback((newVar: ValueSelector | string) => { @@ -109,7 +108,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { query_variable_selector: inputs.query_variable_selector.length > 0 ? inputs.query_variable_selector : query_variable_selector, }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig]) const handleClassesChange = useCallback((newClasses: any) => { @@ -163,59 +162,6 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { setInputs(newInputs) }, [inputs, setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - runInputDataRef, - setRunInputData, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: { - 'query': '', - '#files#': [], - }, - }) - - const query = runInputData.query - const setQuery = useCallback((newQuery: string) => { - setRunInputData({ - ...runInputData, - query: newQuery, - }) - }, [runInputData, setRunInputData]) - - const varInputs = getInputVars([inputs.instruction]) - const inputVarValues = (() => { - const vars: Record = { - query, - } - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record) => { - setRunInputData(newPayload) - }, [setRunInputData]) - - const visionFiles = runInputData['#files#'] - const setVisionFiles = useCallback((newFiles: any[]) => { - setRunInputData({ - ...runInputDataRef.current, - '#files#': newFiles, - }) - }, [runInputDataRef, setRunInputData]) - const filterVar = useCallback((varPayload: Var) => { return varPayload.type === VarType.string }, []) @@ -235,23 +181,10 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { availableNodesWithParent, availableVisionVars, handleInstructionChange, - varInputs, - inputVarValues, - setInputVarValues, handleMemoryChange, isVisionModel, handleVisionResolutionEnabledChange, handleVisionResolutionChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - query, - setQuery, - runResult, - visionFiles, - setVisionFiles, } } diff --git a/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts new file mode 100644 index 000000000..66755abb6 --- /dev/null +++ b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts @@ -0,0 +1,146 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { InputVar, Var, Variable } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' +import type { QuestionClassifierNodeType } from './types' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { useCallback } from 'react' +import useConfigVision from '../../hooks/use-config-vision' +import { noop } from 'lodash-es' +import { findVariableWhenOnLLMVision } from '../utils' +import useAvailableVarList from '../_base/hooks/use-available-var-list' + +const i18nPrefix = 'workflow.nodes.questionClassifiers' + +type Params = { + id: string, + payload: QuestionClassifierNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud(id, payload) + + const model = inputs.model + + const { + isVisionModel, + } = useConfigVision(model, { + payload: inputs.vision, + onChange: noop, + }) + + const visionFiles = runInputData['#files#'] + const setVisionFiles = useCallback((newFiles: any[]) => { + setRunInputData?.({ + ...runInputDataRef.current, + '#files#': newFiles, + }) + }, [runInputDataRef, setRunInputData]) + + const varInputs = getInputVars([inputs.instruction]) + + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .filter(key => !['#files#'].includes(key)) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const setInputVarValues = useCallback((newPayload: Record) => { + const newVars = { + ...newPayload, + '#files#': runInputDataRef.current['#files#'], + } + setRunInputData?.(newVars) + }, [runInputDataRef, setRunInputData]) + + const filterVisionInputVar = useCallback((varPayload: Var) => { + return [VarType.file, VarType.arrayFile].includes(varPayload.type) + }, []) + const { + availableVars: availableVisionVars, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterVisionInputVar, + }) + + const forms = (() => { + const forms: FormProps[] = [] + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: [{ + label: t(`${i18nPrefix}.inputVars`)!, + variable: 'query', + type: InputVarType.paragraph, + required: true, + }, ...varInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ) + + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const currentVariable = findVariableWhenOnLLMVision(payload.vision.configs.variable_selector, availableVisionVars) + + forms.push( + { + label: t('workflow.nodes.llm.vision')!, + inputs: [{ + label: currentVariable?.variable as any, + variable: '#files#', + type: currentVariable?.formType as any, + required: false, + }], + values: { '#files#': visionFiles }, + onChange: keyValue => setVisionFiles(keyValue['#files#']), + }, + ) + } + return forms + })() + + const getDependentVars = () => { + const promptVars = varInputs.map(item => item.variable.slice(1, -1).split('.')) + const vars = [payload.query_variable_selector, ...promptVars] + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const visionVar = payload.vision.configs.variable_selector + vars.push(visionVar) + } + return vars + } + + const getDependentVar = (variable: string) => { + if(variable === 'query') + return payload.query_variable_selector + if(variable === '#files#') + return payload.vision.configs?.variable_selector + + return false + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/start/use-config.ts b/web/app/components/workflow/nodes/start/use-config.ts index e30e8c283..c0ade614e 100644 --- a/web/app/components/workflow/nodes/start/use-config.ts +++ b/web/app/components/workflow/nodes/start/use-config.ts @@ -10,6 +10,7 @@ import { useNodesReadOnly, useWorkflow, } from '@/app/components/workflow/hooks' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: StartNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -18,6 +19,13 @@ const useConfig = (id: string, payload: StartNodeType) => { const { inputs, setInputs } = useNodeCrud(id, payload) + const { + deleteNodeInspectorVars, + renameInspectVarName, + nodesWithInspectVars, + deleteInspectVar, + } = useInspectVarsCrud() + const [isShowAddVarModal, { setTrue: showAddVarModal, setFalse: hideAddVarModal, @@ -31,6 +39,12 @@ const useConfig = (id: string, payload: StartNodeType) => { const [removedIndex, setRemoveIndex] = useState(0) const handleVarListChange = useCallback((newList: InputVar[], moreInfo?: { index: number; payload: MoreInfo }) => { if (moreInfo?.payload?.type === ChangeType.remove) { + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === moreInfo?.payload?.payload?.beforeKey + })?.id + if(varId) + deleteInspectVar(id, varId) + if (isVarUsedInNodes([id, moreInfo?.payload?.payload?.beforeKey || ''])) { showRemoveVarConfirm() setRemovedVar([id, moreInfo?.payload?.payload?.beforeKey || '']) @@ -46,8 +60,12 @@ const useConfig = (id: string, payload: StartNodeType) => { if (moreInfo?.payload?.type === ChangeType.changeVarName) { const changedVar = newList[moreInfo.index] handleOutVarRenameChange(id, [id, inputs.variables[moreInfo.index].variable], [id, changedVar.variable]) + renameInspectVarName(id, inputs.variables[moreInfo.index].variable, changedVar.variable) } - }, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) + else if(moreInfo?.payload?.type !== ChangeType.remove) { // edit var type + deleteNodeInspectorVars(id) + } + }, [deleteInspectVar, deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, nodesWithInspectVars, renameInspectVarName, setInputs, showRemoveVarConfirm]) const removeVarInNode = useCallback(() => { const newInputs = produce(inputs, (draft) => { diff --git a/web/app/components/workflow/nodes/start/use-single-run-form-params.ts b/web/app/components/workflow/nodes/start/use-single-run-form-params.ts new file mode 100644 index 000000000..38abbf2a6 --- /dev/null +++ b/web/app/components/workflow/nodes/start/use-single-run-form-params.ts @@ -0,0 +1,87 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { ValueSelector } from '@/app/components/workflow/types' +import { type InputVar, InputVarType, type Variable } from '@/app/components/workflow/types' +import type { StartNodeType } from './types' +import { useIsChatMode } from '../../hooks' + +type Params = { + id: string, + payload: StartNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const isChatMode = useIsChatMode() + + const forms = (() => { + const forms: FormProps[] = [] + const inputs: InputVar[] = payload.variables.map((item) => { + return { + ...item, + getVarValueFromDependent: true, + } + }) + + if (isChatMode) { + inputs.push({ + label: 'sys.query', + variable: '#sys.query#', + type: InputVarType.textInput, + required: true, + }) + } + + inputs.push({ + label: 'sys.files', + variable: '#sys.files#', + type: InputVarType.multiFiles, + required: false, + }) + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs, + values: runInputData, + onChange: setRunInputData, + }, + ) + + return forms + })() + + const getDependentVars = () => { + const inputVars = payload.variables.map((item) => { + return [id, item.variable] + }) + const vars: ValueSelector[] = [...inputVars, ['sys', 'files']] + + if (isChatMode) + vars.push(['sys', 'query']) + + return vars + } + + const getDependentVar = (variable: string) => { + return [id, variable] + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/template-transform/panel.tsx b/web/app/components/workflow/nodes/template-transform/panel.tsx index e12048292..29c34ee66 100644 --- a/web/app/components/workflow/nodes/template-transform/panel.tsx +++ b/web/app/components/workflow/nodes/template-transform/panel.tsx @@ -14,8 +14,6 @@ import Split from '@/app/components/workflow/nodes/_base/components/split' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import type { NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.templateTransform' @@ -35,16 +33,6 @@ const Panel: FC> = ({ handleAddEmptyVariable, handleCodeChange, filterVar, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, } = useConfig(id, data) return ( @@ -106,23 +94,6 @@ const Panel: FC> = ({
- {isShowSingleRun && ( - } - /> - )}
) } diff --git a/web/app/components/workflow/nodes/template-transform/use-config.ts b/web/app/components/workflow/nodes/template-transform/use-config.ts index e0c41ac2d..8be93abdf 100644 --- a/web/app/components/workflow/nodes/template-transform/use-config.ts +++ b/web/app/components/workflow/nodes/template-transform/use-config.ts @@ -6,7 +6,6 @@ import { VarType } from '../../types' import { useStore } from '../../store' import type { TemplateTransformNodeType } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useNodesReadOnly, } from '@/app/components/workflow/hooks' @@ -66,7 +65,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { ...defaultConfig, }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig]) const handleCodeChange = useCallback((template: string) => { @@ -76,37 +75,6 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { setInputs(newInputs) }, [setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: {}, - }) - const varInputs = toVarInputs(inputs.variables) - - const inputVarValues = (() => { - const vars: Record = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record) => { - setRunInputData(newPayload) - }, [setRunInputData]) - const filterVar = useCallback((varPayload: Var) => { return [VarType.string, VarType.number, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type) }, []) @@ -121,16 +89,6 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { handleAddEmptyVariable, handleCodeChange, filterVar, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, } } diff --git a/web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts b/web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts new file mode 100644 index 000000000..ab1cfe731 --- /dev/null +++ b/web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts @@ -0,0 +1,65 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { TemplateTransformNodeType } from './types' + +type Params = { + id: string, + payload: TemplateTransformNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + toVarInputs, + setRunInputData, +}: Params) => { + const { inputs } = useNodeCrud(id, payload) + + const varInputs = toVarInputs(inputs.variables) + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + }, [inputVarValues, setInputVarValues, varInputs]) + + const getDependentVars = () => { + return payload.variables.map(v => v.value_selector) + } + + const getDependentVar = (variable: string) => { + const varItem = payload.variables.find(v => v.variable === variable) + if (varItem) + return varItem.value_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 393a11c1e..5dd5242c7 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import React, { useMemo } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import Split from '../_base/components/split' import type { ToolNodeType } from './types' @@ -11,12 +11,7 @@ import type { NodePanelProps } from '@/app/components/workflow/types' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import Loading from '@/app/components/base/loading' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import ResultPanel from '@/app/components/workflow/run/result-panel' -import { useToolIcon } from '@/app/components/workflow/hooks' -import { useLogs } from '@/app/components/workflow/run/hooks' -import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log' import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' import { Type } from '../llm/types' @@ -45,23 +40,9 @@ const Panel: FC> = ({ hideSetAuthModal, handleSaveAuth, isLoading, - isShowSingleRun, - hideSingleRun, - singleRunForms, - runningStatus, - handleRun, - handleStop, - runResult, outputSchema, hasObjectOutput, } = useConfig(id, data) - const toolIcon = useToolIcon(data) - const logsParams = useLogs() - const nodeInfo = useMemo(() => { - if (!runResult) - return null - return formatToTracingNodeList([runResult], t)[0] - }, [runResult, t]) if (isLoading) { return
@@ -180,21 +161,6 @@ const Panel: FC> = ({
- - {isShowSingleRun && ( - } - /> - )}
) } diff --git a/web/app/components/workflow/nodes/tool/use-config.ts b/web/app/components/workflow/nodes/tool/use-config.ts index 38ca5b519..b83ae8a07 100644 --- a/web/app/components/workflow/nodes/tool/use-config.ts +++ b/web/app/components/workflow/nodes/tool/use-config.ts @@ -3,17 +3,15 @@ import { useTranslation } from 'react-i18next' import produce from 'immer' import { useBoolean } from 'ahooks' import { useStore } from '../../store' -import { type ToolNodeType, type ToolVarInputs, VarType } from './types' +import type { ToolNodeType, ToolVarInputs } from './types' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { CollectionType } from '@/app/components/tools/types' import { updateBuiltInToolCredential } from '@/service/tools' import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import Toast from '@/app/components/base/toast' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import { VarType as VarVarType } from '@/app/components/workflow/types' -import type { InputVar, ValueSelector, Var } from '@/app/components/workflow/types' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' +import type { InputVar, Var } from '@/app/components/workflow/types' import { useFetchToolsData, useNodesReadOnly, @@ -160,39 +158,8 @@ const useConfig = (id: string, payload: ToolNodeType) => { const isLoading = currTool && (isBuiltIn ? !currCollection : false) - // single run - const [inputVarValues, doSetInputVarValues] = useState>({}) - const setInputVarValues = (value: Record) => { - doSetInputVarValues(value) - // eslint-disable-next-line ts/no-use-before-define - setRunInputData(value) - } - // fill single run form variable with constant value first time - const inputVarValuesWithConstantValue = () => { - const res = produce(inputVarValues, (draft) => { - Object.keys(inputs.tool_parameters).forEach((key: string) => { - const { type, value } = inputs.tool_parameters[key] - if (type === VarType.constant && (value === undefined || value === null)) - draft.tool_parameters[key].value = value - }) - }) - return res - } - - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - setRunInputData, - handleRun: doHandleRun, - handleStop, - runResult, - } = useOneStepRun({ - id, - data: inputs, - defaultRunInputData: {}, - moreDataForCheckValid: { + const getMoreDataForCheckValid = () => { + return { toolInputsSchema: (() => { const formInputs: InputVar[] = [] toolInputVarSchema.forEach((item: any) => { @@ -208,52 +175,7 @@ const useConfig = (id: string, payload: ToolNodeType) => { notAuthed: isShowAuthBtn, toolSettingSchema, language, - }, - }) - - const hadVarParams = Object.keys(inputs.tool_parameters) - .filter(key => inputs.tool_parameters[key].type !== VarType.constant) - .map(k => inputs.tool_parameters[k]) - - const varInputs = getInputVars(hadVarParams.map((p) => { - if (p.type === VarType.variable) { - // handle the old wrong value not crash the page - if (!(p.value as any).join) - return `{{#${p.value}#}}` - - return `{{#${(p.value as ValueSelector).join('.')}#}}` } - - return p.value as string - })) - - const singleRunForms = (() => { - const forms: FormProps[] = [{ - inputs: varInputs, - values: inputVarValuesWithConstantValue(), - onChange: setInputVarValues, - }] - return forms - })() - - const handleRun = (submitData: Record) => { - const varTypeInputKeys = Object.keys(inputs.tool_parameters) - .filter(key => inputs.tool_parameters[key].type === VarType.variable) - const shouldAdd = varTypeInputKeys.length > 0 - if (!shouldAdd) { - doHandleRun(submitData) - return - } - const addMissedVarData = { ...submitData } - Object.keys(submitData).forEach((key) => { - const value = submitData[key] - varTypeInputKeys.forEach((inputKey) => { - const inputValue = inputs.tool_parameters[inputKey].value as ValueSelector - if (`#${inputValue.join('.')}#` === key) - addMissedVarData[inputKey] = value - }) - }) - doHandleRun(addMissedVarData) } const outputSchema = useMemo(() => { @@ -307,18 +229,9 @@ const useConfig = (id: string, payload: ToolNodeType) => { hideSetAuthModal, handleSaveAuth, isLoading, - isShowSingleRun, - hideSingleRun, - inputVarValues, - varInputs, - setInputVarValues, - singleRunForms, - runningStatus, - handleRun, - handleStop, - runResult, outputSchema, hasObjectOutput, + getMoreDataForCheckValid, } } diff --git a/web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts b/web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts new file mode 100644 index 000000000..a68f12fc3 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts @@ -0,0 +1,20 @@ +import type { ToolNodeType } from './types' +import useConfig from './use-config' + +type Params = { + id: string + payload: ToolNodeType, +} + +const useGetDataForCheckMore = ({ + id, + payload, +}: Params) => { + const { getMoreDataForCheckValid } = useConfig(id, payload) + + return { + getData: getMoreDataForCheckValid, + } +} + +export default useGetDataForCheckMore diff --git a/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts new file mode 100644 index 000000000..295cf0263 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts @@ -0,0 +1,94 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo, useState } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { type ToolNodeType, VarType } from './types' +import type { ValueSelector } from '@/app/components/workflow/types' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import produce from 'immer' +import type { NodeTracing } from '@/types/workflow' +import { useTranslation } from 'react-i18next' +import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log' +import { useToolIcon } from '../../hooks' + +type Params = { + id: string, + payload: ToolNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] + runResult: NodeTracing +} +const useSingleRunFormParams = ({ + id, + payload, + getInputVars, + setRunInputData, + runResult, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud(id, payload) + + const hadVarParams = Object.keys(inputs.tool_parameters) + .filter(key => inputs.tool_parameters[key].type !== VarType.constant) + .map(k => inputs.tool_parameters[k]) + const varInputs = getInputVars(hadVarParams.map((p) => { + if (p.type === VarType.variable) { + // handle the old wrong value not crash the page + if (!(p.value as any).join) + return `{{#${p.value}#}}` + + return `{{#${(p.value as ValueSelector).join('.')}#}}` + } + + return p.value as string + })) + const [inputVarValues, doSetInputVarValues] = useState>({}) + const setInputVarValues = useCallback((value: Record) => { + doSetInputVarValues(value) + setRunInputData(value) + }, [setRunInputData]) + + const inputVarValuesWithConstantValue = useCallback(() => { + const res = produce(inputVarValues, (draft) => { + Object.keys(inputs.tool_parameters).forEach((key: string) => { + const { type, value } = inputs.tool_parameters[key] + if (type === VarType.constant && (value === undefined || value === null)) + draft[key] = value + }) + }) + return res + }, [inputs.tool_parameters, inputVarValues]) + + const forms = useMemo(() => { + const forms: FormProps[] = [{ + inputs: varInputs, + values: inputVarValuesWithConstantValue(), + onChange: setInputVarValues, + }] + return forms + }, [inputVarValuesWithConstantValue, setInputVarValues, varInputs]) + + const nodeInfo = useMemo(() => { + if (!runResult) + return null + return formatToTracingNodeList([runResult], t)[0] + }, [runResult, t]) + + const toolIcon = useToolIcon(payload) + + const getDependentVars = () => { + return varInputs.map(item => item.variable.slice(1, -1).split('.')) + } + + return { + forms, + nodeInfo, + toolIcon, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/variable-assigner/use-config.ts b/web/app/components/workflow/nodes/variable-assigner/use-config.ts index f5a7a092b..c65941e32 100644 --- a/web/app/components/workflow/nodes/variable-assigner/use-config.ts +++ b/web/app/components/workflow/nodes/variable-assigner/use-config.ts @@ -1,6 +1,6 @@ -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' +import { useBoolean, useDebounceFn } from 'ahooks' import { v4 as uuid4 } from 'uuid' import type { ValueSelector, Var } from '../../types' import { VarType } from '../../types' @@ -12,8 +12,13 @@ import { useNodesReadOnly, useWorkflow, } from '@/app/components/workflow/hooks' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: VariableAssignerNodeType) => { + const { + deleteNodeInspectorVars, + renameInspectVarName, + } = useInspectVarsCrud() const { nodesReadOnly: readOnly } = useNodesReadOnly() const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow() @@ -113,7 +118,8 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { draft.advanced_settings.group_enabled = enabled }) setInputs(newInputs) - }, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) const handleAddGroup = useCallback(() => { let maxInGroupName = 1 @@ -134,7 +140,22 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { }) }) setInputs(newInputs) - }, [inputs, setInputs]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, id, inputs, setInputs]) + + // record the first old name value + const oldNameRecord = useRef>({}) + + const { + run: renameInspectNameWithDebounce, + } = useDebounceFn( + (id: string, newName: string) => { + const oldName = oldNameRecord.current[id] + renameInspectVarName(id, oldName, newName) + delete oldNameRecord.current[id] + }, + { wait: 500 }, + ) const handleVarGroupNameChange = useCallback((groupId: string) => { return (name: string) => { @@ -144,8 +165,11 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { }) handleOutVarRenameChange(id, [id, inputs.advanced_settings.groups[index].group_name, 'output'], [id, name, 'output']) setInputs(newInputs) + if(!(id in oldNameRecord.current)) + oldNameRecord.current[id] = inputs.advanced_settings.groups[index].group_name + renameInspectNameWithDebounce(id, name) } - }, [handleOutVarRenameChange, id, inputs, setInputs]) + }, [handleOutVarRenameChange, id, inputs, renameInspectNameWithDebounce, setInputs]) const onRemoveVarConfirm = useCallback(() => { removedVars.forEach((v) => { diff --git a/web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts b/web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts new file mode 100644 index 000000000..0d6d737c2 --- /dev/null +++ b/web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts @@ -0,0 +1,92 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useCallback } from 'react' +import type { VariableAssignerNodeType } from './types' + +type Params = { + id: string, + payload: VariableAssignerNodeType, + runInputData: Record + runInputDataRef: MutableRefObject> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, + varSelectorsToVarInputs, +}: Params) => { + const setInputVarValues = useCallback((newPayload: Record) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = (() => { + const allInputs: ValueSelector[] = [] + const isGroupEnabled = !!payload.advanced_settings?.group_enabled + if (!isGroupEnabled && payload.variables && payload.variables.length) { + payload.variables.forEach((varSelector) => { + allInputs.push(varSelector) + }) + } + if (isGroupEnabled && payload.advanced_settings && payload.advanced_settings.groups && payload.advanced_settings.groups.length) { + payload.advanced_settings.groups.forEach((group) => { + group.variables?.forEach((varSelector) => { + allInputs.push(varSelector) + }) + }) + } + + const varInputs = varSelectorsToVarInputs(allInputs) + // remove duplicate inputs + const existVarsKey: Record = {} + const uniqueVarInputs: InputVar[] = [] + varInputs.forEach((input) => { + if(!input) + return + if (!existVarsKey[input.variable]) { + existVarsKey[input.variable] = true + uniqueVarInputs.push({ + ...input, + required: false, // just one of the inputs is required + }) + } + }) + return [ + { + inputs: uniqueVarInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + })() + + const getDependentVars = () => { + if(payload.advanced_settings?.group_enabled) { + const vars: ValueSelector[][] = [] + payload.advanced_settings.groups.forEach((group) => { + if(group.variables) + vars.push([...group.variables]) + }) + return vars + } + return [payload.variables] + } + + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index d35a5be8b..5bc541a45 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -96,7 +96,7 @@ const AddBlock = ({ onOpenChange={handleOpenChange} disabled={nodesReadOnly} onSelect={handleSelect} - placement='top-start' + placement='right-start' offset={offset ?? { mainAxis: 4, crossAxis: -8, diff --git a/web/app/components/workflow/operator/control.tsx b/web/app/components/workflow/operator/control.tsx index 5f7d19a17..7967bf0a6 100644 --- a/web/app/components/workflow/operator/control.tsx +++ b/web/app/components/workflow/operator/control.tsx @@ -4,6 +4,8 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { + RiAspectRatioFill, + RiAspectRatioLine, RiCursorLine, RiFunctionAddLine, RiHand, @@ -11,6 +13,7 @@ import { } from '@remixicon/react' import { useNodesReadOnly, + useWorkflowCanvasMaximize, useWorkflowMoveMode, useWorkflowOrganize, } from '../hooks' @@ -28,6 +31,7 @@ import cn from '@/utils/classnames' const Control = () => { const { t } = useTranslation() const controlMode = useStore(s => s.controlMode) + const maximizeCanvas = useStore(s => s.maximizeCanvas) const { handleModePointer, handleModeHand } = useWorkflowMoveMode() const { handleLayout } = useWorkflowOrganize() const { handleAddNote } = useOperator() @@ -35,6 +39,7 @@ const Control = () => { nodesReadOnly, getNodesReadOnly, } = useNodesReadOnly() + const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize() const addNote = (e: MouseEvent) => { if (getNodesReadOnly()) @@ -45,7 +50,7 @@ const Control = () => { } return ( -
+
{
- +
{
- +
{
+ +
+ {maximizeCanvas && } + {!maximizeCanvas && } +
+
) } diff --git a/web/app/components/workflow/operator/index.tsx b/web/app/components/workflow/operator/index.tsx index 94ea8143e..4a472a755 100644 --- a/web/app/components/workflow/operator/index.tsx +++ b/web/app/components/workflow/operator/index.tsx @@ -1,8 +1,10 @@ -import { memo } from 'react' +import { memo, useEffect, useMemo, useRef } from 'react' import { MiniMap } from 'reactflow' import UndoRedo from '../header/undo-redo' import ZoomInOut from './zoom-in-out' -import Control from './control' +import VariableTrigger from '../variable-inspect/trigger' +import VariableInspectPanel from '../variable-inspect' +import { useStore } from '../store' export type OperatorProps = { handleUndo: () => void @@ -10,25 +12,65 @@ export type OperatorProps = { } const Operator = ({ handleUndo, handleRedo }: OperatorProps) => { + const bottomPanelRef = useRef(null) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const rightPanelWidth = useStore(s => s.rightPanelWidth) + const setBottomPanelWidth = useStore(s => s.setBottomPanelWidth) + const setBottomPanelHeight = useStore(s => s.setBottomPanelHeight) + + const bottomPanelWidth = useMemo(() => { + if (!workflowCanvasWidth || !rightPanelWidth) + return 'auto' + return Math.max((workflowCanvasWidth - rightPanelWidth), 400) + }, [workflowCanvasWidth, rightPanelWidth]) + + // update bottom panel height + useEffect(() => { + if (bottomPanelRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize, blockSize } = entry.borderBoxSize[0] + setBottomPanelWidth(inlineSize) + setBottomPanelHeight(blockSize) + } + }) + resizeContainerObserver.observe(bottomPanelRef.current) + return () => { + resizeContainerObserver.disconnect() + } + } + }, [setBottomPanelHeight, setBottomPanelWidth]) + return ( - <> - -
- +
+
- + +
+ + +
- + +
) } diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index d8da0e69a..9c82ee748 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -377,7 +377,7 @@ const ChatVariableModal = ({
{t('workflow.chatVariable.modal.description')}