Browse Source

feat: last run frontend (#21369)

The frontend of feat: Persist Variables for Enhanced Debugging Workflow (#20699).

Co-authored-by: jZonG <jzongcode@gmail.com>
tags/1.5.0
Joel 4 months ago
parent
commit
1a1bfd4048
No account linked to committer's email address
100 changed files with 4228 additions and 2055 deletions
  1. 48
    35
      web/app/components/app-sidebar/app-info.tsx
  2. 125
    0
      web/app/components/app-sidebar/app-sidebar-dropdown.tsx
  3. 24
    1
      web/app/components/app-sidebar/index.tsx
  4. 11
    5
      web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx
  5. 1
    1
      web/app/components/base/file-uploader/utils.ts
  6. 7
    3
      web/app/components/base/tab-header/index.tsx
  7. 15
    0
      web/app/components/header/header-wrapper.tsx
  8. 68
    0
      web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts
  9. 0
    1
      web/app/components/workflow-app/hooks/use-workflow-init.ts
  10. 13
    21
      web/app/components/workflow-app/hooks/use-workflow-run.ts
  11. 2
    0
      web/app/components/workflow/constants.ts
  12. 11
    1
      web/app/components/workflow/header/header-in-restoring.tsx
  13. 6
    1
      web/app/components/workflow/header/index.tsx
  14. 13
    1
      web/app/components/workflow/header/run-and-history.tsx
  15. 241
    0
      web/app/components/workflow/hooks/use-inspect-vars-crud.ts
  16. 35
    0
      web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts
  17. 8
    2
      web/app/components/workflow/hooks/use-nodes-interactions.ts
  18. 12
    0
      web/app/components/workflow/hooks/use-shortcuts.ts
  19. 26
    0
      web/app/components/workflow/hooks/use-workflow-interactions.ts
  20. 2
    5
      web/app/components/workflow/hooks/use-workflow.ts
  21. 40
    0
      web/app/components/workflow/index.tsx
  22. 7
    6
      web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
  23. 5
    1
      web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx
  24. 56
    85
      web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx
  25. 41
    0
      web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx
  26. 12
    9
      web/app/components/workflow/nodes/_base/components/node-control.tsx
  27. 4
    2
      web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx
  28. 429
    0
      web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
  29. 126
    0
      web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx
  30. 36
    0
      web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx
  31. 330
    0
      web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts
  32. 34
    0
      web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx
  33. 143
    28
      web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts
  34. 47
    6
      web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts
  35. 8
    4
      web/app/components/workflow/nodes/_base/node.tsx
  36. 0
    214
      web/app/components/workflow/nodes/_base/panel.tsx
  37. 1
    53
      web/app/components/workflow/nodes/agent/panel.tsx
  38. 0
    42
      web/app/components/workflow/nodes/agent/use-config.ts
  39. 90
    0
      web/app/components/workflow/nodes/agent/use-single-run-form-params.ts
  40. 1
    0
      web/app/components/workflow/nodes/assigner/components/var-list/index.tsx
  41. 2
    0
      web/app/components/workflow/nodes/assigner/types.ts
  42. 1
    1
      web/app/components/workflow/nodes/assigner/use-config.ts
  43. 55
    0
      web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts
  44. 0
    31
      web/app/components/workflow/nodes/code/panel.tsx
  45. 1
    45
      web/app/components/workflow/nodes/code/use-config.ts
  46. 65
    0
      web/app/components/workflow/nodes/code/use-single-run-form-params.ts
  47. 1
    36
      web/app/components/workflow/nodes/document-extractor/panel.tsx
  48. 1
    45
      web/app/components/workflow/nodes/document-extractor/use-config.ts
  49. 64
    0
      web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts
  50. 0
    30
      web/app/components/workflow/nodes/http/panel.tsx
  51. 1
    61
      web/app/components/workflow/nodes/http/use-config.ts
  52. 74
    0
      web/app/components/workflow/nodes/http/use-single-run-form-params.ts
  53. 1
    1
      web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx
  54. 166
    0
      web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts
  55. 2
    2
      web/app/components/workflow/nodes/index.tsx
  56. 1
    54
      web/app/components/workflow/nodes/iteration/panel.tsx
  57. 1
    0
      web/app/components/workflow/nodes/iteration/types.ts
  58. 14
    157
      web/app/components/workflow/nodes/iteration/use-config.ts
  59. 154
    0
      web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts
  60. 1
    33
      web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx
  61. 3
    38
      web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
  62. 63
    0
      web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts
  63. 35
    26
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx
  64. 10
    1
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx
  65. 25
    0
      web/app/components/workflow/nodes/llm/default.ts
  66. 1
    82
      web/app/components/workflow/nodes/llm/panel.tsx
  67. 7
    92
      web/app/components/workflow/nodes/llm/use-config.ts
  68. 198
    0
      web/app/components/workflow/nodes/llm/use-single-run-form-params.ts
  69. 1
    43
      web/app/components/workflow/nodes/loop/panel.tsx
  70. 2
    150
      web/app/components/workflow/nodes/loop/use-config.ts
  71. 221
    0
      web/app/components/workflow/nodes/loop/use-single-run-form-params.ts
  72. 1
    66
      web/app/components/workflow/nodes/parameter-extractor/panel.tsx
  73. 15
    71
      web/app/components/workflow/nodes/parameter-extractor/use-config.ts
  74. 148
    0
      web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts
  75. 1
    66
      web/app/components/workflow/nodes/question-classifier/panel.tsx
  76. 2
    69
      web/app/components/workflow/nodes/question-classifier/use-config.ts
  77. 146
    0
      web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts
  78. 19
    1
      web/app/components/workflow/nodes/start/use-config.ts
  79. 87
    0
      web/app/components/workflow/nodes/start/use-single-run-form-params.ts
  80. 0
    29
      web/app/components/workflow/nodes/template-transform/panel.tsx
  81. 1
    43
      web/app/components/workflow/nodes/template-transform/use-config.ts
  82. 65
    0
      web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts
  83. 1
    35
      web/app/components/workflow/nodes/tool/panel.tsx
  84. 5
    92
      web/app/components/workflow/nodes/tool/use-config.ts
  85. 20
    0
      web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts
  86. 94
    0
      web/app/components/workflow/nodes/tool/use-single-run-form-params.ts
  87. 29
    5
      web/app/components/workflow/nodes/variable-assigner/use-config.ts
  88. 92
    0
      web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts
  89. 1
    1
      web/app/components/workflow/operator/add-block.tsx
  90. 21
    3
      web/app/components/workflow/operator/control.tsx
  91. 60
    18
      web/app/components/workflow/operator/index.tsx
  92. 1
    1
      web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx
  93. 16
    5
      web/app/components/workflow/panel/chat-variable-panel/index.tsx
  94. 8
    0
      web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
  95. 8
    0
      web/app/components/workflow/panel/debug-and-preview/hooks.ts
  96. 72
    80
      web/app/components/workflow/panel/debug-and-preview/index.tsx
  97. 53
    10
      web/app/components/workflow/panel/index.tsx
  98. 11
    3
      web/app/components/workflow/panel/version-history-panel/index.tsx
  99. 2
    2
      web/app/components/workflow/run/result-panel.tsx
  100. 0
    0
      web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts

+ 48
- 35
web/app/components/app-sidebar/app-info.tsx View File

@@ -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 (
<div>
<button
onClick={() => {
if (isCurrentWorkspaceEditor)
setOpen(v => !v)
}}
className='block w-full'
>
<div className={cn('flex rounded-lg', expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}>
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}>
<AppIcon
size={expand ? 'large' : 'small'}
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
{!onlyShowDetail && (
<button
onClick={() => {
if (isCurrentWorkspaceEditor)
setOpen(v => !v)
}}
className='block w-full'
>
<div className={cn('flex rounded-lg', expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}>
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}>
<AppIcon
size={expand ? 'large' : 'small'}
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
</div>
{
expand && (
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
{
expand && (
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{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')}</div>
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{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')}</div>
</div>
)
}
</div>
</button>
)
}
</div>
</button>
)}
<ContentDialog
show={open}
onClose={() => 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'
>
<div className='flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4'>
@@ -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)
}}>
<RiFileCopy2Line className='h-3.5 w-3.5 text-components-button-secondary-text' />
@@ -308,6 +318,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
&& <div className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowImportDSLModal(true)
}}>
<RiFileUploadLine className='h-4 w-4 text-text-tertiary' />
@@ -319,6 +330,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
&& <div className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowSwitchModal(true)
}}>
<RiExchange2Line className='h-4 w-4 text-text-tertiary' />
@@ -345,6 +357,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
className='gap-0.5'
onClick={() => {
setOpen(false)
onDetailExpand?.(false)
setShowConfirmDelete(true)
}}
>

+ 125
- 0
web/app/components/app-sidebar/app-sidebar-dropdown.tsx View File

@@ -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 (
<>
<div className='fixed left-2 top-2 z-20'>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={{
mainAxis: -41,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div className={cn('flex cursor-pointer items-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-1 shadow-lg backdrop-blur-sm hover:bg-background-default-hover', open && 'bg-background-default-hover')}>
<AppIcon
size='small'
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<RiMenuLine className='h-4 w-4 text-text-tertiary' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={cn('w-[305px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg')}>
<div className='p-2'>
<div
className={cn('flex flex-col gap-2 rounded-lg p-2 pb-2.5', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}
onClick={() => {
setDetailExpand(true)
setOpen(false)
}}
>
<div className='flex items-center justify-between self-stretch'>
<AppIcon
size='large'
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{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')}</div>
</div>
</div>
</div>
<div className='px-4'>
<Divider bgStyle='gradient' />
</div>
<nav className='space-y-0.5 px-3 pb-6 pt-4'>
{navigation.map((item, index) => {
return (
<NavLink key={index} mode='expand' iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
)
})}
</nav>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
<div className='z-20'>
<AppInfo expand onlyShowDetail openState={detailExpand} onDetailExpand={setDetailExpand} />
</div>
</>
)
}

export default AppSidebarDropdown

+ 24
- 1
web/app/components/app-sidebar/index.tsx View File

@@ -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 (
<div className='flex w-0 shrink-0'>
<AppSidebarDropdown navigation={navigation} />
</div>
)
}

return (
<div
className={`

+ 11
- 5
web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx View File

@@ -26,9 +26,11 @@ type Option = {
icon: React.JSX.Element
}
type FileUploaderInAttachmentProps = {
isDisabled?: boolean
fileConfig: FileUpload
}
const FileUploaderInAttachment = ({
isDisabled,
fileConfig,
}: FileUploaderInAttachmentProps) => {
const { t } = useTranslation()
@@ -89,16 +91,18 @@ const FileUploaderInAttachment = ({

return (
<div>
<div className='flex items-center space-x-1'>
{options.map(renderOption)}
</div>
{!isDisabled && (
<div className='flex items-center space-x-1'>
{options.map(renderOption)}
</div>
)}
<div className='mt-1 space-y-1'>
{
files.map(file => (
<FileItem
key={file.id}
file={file}
showDeleteAction
showDeleteAction={!isDisabled}
showDownloadAction={false}
onRemove={() => 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 (
<FileContextProvider
value={value}
onChange={onChange}
>
<FileUploaderInAttachment fileConfig={fileConfig} />
<FileUploaderInAttachment isDisabled={isDisabled} fileConfig={fileConfig} />
</FileContextProvider>
)
}

+ 1
- 1
web/app/components/base/file-uploader/utils.ts View File

@@ -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,
}
})
}

+ 7
- 3
web/app/components/base/tab-header/index.tsx View File

@@ -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<ITabHeaderProps> = ({
items,
value,
itemClassName,
onChange,
}) => {
const renderItem = ({ id, name, icon, extra }: Item) => (
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
<div
key={id}
className={cn(
'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5',
id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary',
disabled && 'cursor-not-allowed opacity-30',
)}
onClick={() => onChange(id)}
onClick={() => !disabled && onChange(id)}
>
{icon || ''}
<div className='ml-2'>{name}</div>
<div className={cn('ml-2', itemClassName)}>{name}</div>
{extra || ''}
</div>
)

+ 15
- 0
web/app/components/header/header-wrapper.tsx View File

@@ -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 (
<div className={classNames(

+ 68
- 0
web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts View File

@@ -0,0 +1,68 @@
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { useWorkflowStore } from '../../workflow/store'
import { useStoreApi } from 'reactflow'
import type { Node } from '@/app/components/workflow/types'
import { fetchAllInspectVars } from '@/service/workflow'
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
import { useNodesInteractionsWithoutSync } from '../../workflow/hooks/use-nodes-interactions-without-sync'
const useSetWorkflowVarsWithValue = () => {
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<string, Node> = {}
nodeArr.forEach((node) => {
nodesKeyValue[node.id] = node
})

const withValueNodeIds: Record<string, boolean> = {}
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

+ 0
- 1
web/app/components/workflow-app/hooks/use-workflow-init.ts View File

@@ -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 {

+ 13
- 21
web/app/components/workflow-app/hooks/use-workflow-run.ts View File

@@ -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) => {

+ 2
- 0
web/app/components/workflow/constants.ts View File

@@ -31,6 +31,7 @@ type NodesExtraData = {
getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[]
getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[]
checkValid: any
defaultRunInputData?: Record<string, any>
}
export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
[BlockEnum.Start]: {
@@ -68,6 +69,7 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
getAvailablePrevNodes: LLMDefault.getAvailablePrevNodes,
getAvailableNextNodes: LLMDefault.getAvailableNextNodes,
checkValid: LLMDefault.checkValid,
defaultRunInputData: LLMDefault.defaultRunInputData,
},
[BlockEnum.KnowledgeRetrieval]: {
author: 'Dify',

+ 11
- 1
web/app/components/workflow/header/header-in-restoring.tsx View File

@@ -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 (
<>

+ 6
- 1
web/app/components/workflow/header/index.tsx View File

@@ -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 (
<div
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
>
{inWorkflowCanvas && maximizeCanvas && <div className='h-14 w-[52px]' />}
{
normal && (
<HeaderInNormal

+ 13
- 1
web/app/components/workflow/header/run-and-history.tsx View File

@@ -19,6 +19,8 @@ import cn from '@/utils/classnames'
import {
StopCircle,
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'

const RunMode = memo(() => {
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 (
<>
<div
@@ -59,7 +71,7 @@ const RunMode = memo(() => {
isRunning && (
<div
className='ml-0.5 flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-black/5'
onClick={() => handleStopRun(workflowRunningData?.task_id || '')}
onClick={handleStop}
>
<StopCircle className='h-4 w-4 text-components-button-ghost-text' />
</div>

+ 241
- 0
web/app/components/workflow/hooks/use-inspect-vars-crud.ts View File

@@ -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<string | null>(null)
const [currEditVarId, setCurrEditVarId] = useState<string | null>(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

+ 35
- 0
web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts View File

@@ -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,
}
}

+ 8
- 2
web/app/components/workflow/hooks/use-nodes-interactions.ts View File

@@ -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<OnNodeAdd>((
{

+ 12
- 0
web/app/components/workflow/hooks/use-shortcuts.ts View File

@@ -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()

+ 26
- 0
web/app/components/workflow/hooks/use-workflow-interactions.ts View File

@@ -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,
}
}

+ 2
- 5
web/app/components/workflow/hooks/use-workflow.ts View File

@@ -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)

+ 40
- 0
web/app/components/workflow/index.tsx View File

@@ -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<WorkflowProps> = 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<WorkflowProps> = 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<WorkflowProps> = memo(({
>
<SyncingDataModal />
<CandidateNode />
<div
className='absolute left-0 top-0 z-10 flex w-12 items-center justify-center p-1 pl-2'
style={{ height: controlHeight }}
>
<Control />
</div>
<Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
<PanelContextmenu />
<NodeContextmenu />

+ 7
- 6
web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx View File

@@ -95,6 +95,7 @@ const FormItem: FC<Props> = ({
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<Props> = ({
}}
/>
)}
{(type === InputVarType.multiFiles) && (
{(type === InputVarType.multiFiles || isIteratorItemFile) && (
<FileUploaderInAttachmentWrapper
value={value}
onChange={files => onChange(files)}
fileConfig={{
allowed_file_types: inStepRun
allowed_file_types: (inStepRun || isIteratorItemFile)
? [
SupportUploadFileTypes.image,
SupportUploadFileTypes.document,
@@ -215,7 +216,7 @@ const FormItem: FC<Props> = ({
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<Props> = ({
...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<Props> = ({
}

{
isIterator && (
(isIterator && !isIteratorItemFile) && (
<div className='space-y-2'>
{(value || []).map((item: any, index: number) => (
<TextEditor

+ 5
- 1
web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx View File

@@ -61,10 +61,14 @@ const Form: FC<Props> = ({
}
}, [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<Props> = ({
{label && (
<div className='mb-1 flex items-center justify-between'>
<div className='system-xs-medium-uppercase flex h-6 items-center text-text-tertiary'>{label}</div>
{isArrayLikeType && (
{isArrayLikeType && !isIteratorItemFile && (
<AddButton onClick={handleAddContext} />
)}
</div>

+ 56
- 85
web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx View File

@@ -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<string, any>) => void
onStop: () => void
runningStatus: NodeRunningStatus
result?: React.JSX.Element
forms: FormProps[]
showSpecialResultPanel?: boolean
existVarValuesInForms: Record<string, any>[]
filteredExistVarForms: FormProps[]
} & Partial<SpecialResultPanelProps>

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<BeforeRunFormProps> = ({
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<BeforeRunFormProps> = ({

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<BeforeRunFormProps> = ({
}

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 (
<div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt pt-10'>
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
<div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'>
<div className='truncate text-base font-semibold text-text-primary'>
{t(`${i18nPrefix}.testRun`)} {nodeName}
</div>
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => {
onHide()
}}>
<RiCloseLine className='h-4 w-4 text-text-tertiary ' />
</div>
</div>
{
showSpecialResultPanel && (
<div className='h-0 grow overflow-y-auto pb-4'>
<SpecialResultPanel {...restResultPanelParams} />
</div>
)
}
{
!showSpecialResultPanel && (
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 space-y-4 px-4'>
{forms.map((form, index) => (
<div key={index}>
<Form
key={index}
className={cn(index < forms.length - 1 && 'mb-4')}
{...form}
/>
{index < forms.length - 1 && <Split />}
</div>
))}
</div>
<div className='mt-4 flex justify-between space-x-2 px-4' >
{isRunning && (
<div
className='cursor-pointer rounded-lg border border-divider-regular bg-components-button-secondary-bg p-2 shadow-xs'
onClick={onStop}
>
<StopCircle className='h-4 w-4 text-text-tertiary' />
</div>
)}
<Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
{isRunning && <RiLoader2Line className='h-4 w-4 animate-spin' />}
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
</Button>
</div>
{isRunning && (
<ResultPanel status='running' showSteps={false} />
)}
{isFinished && (
<>
{result}
</>
)}
<PanelWrap
nodeName={nodeName}
onHide={onHide}
>
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 space-y-4 px-4'>
{filteredExistVarForms.map((form, index) => (
<div key={index}>
<Form
key={index}
className={cn(index < forms.length - 1 && 'mb-4')}
{...form}
/>
{index < forms.length - 1 && <Split />}
</div>
)
}
))}
</div>
<div className='mt-4 flex justify-between space-x-2 px-4' >
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
<div>{t(`${i18nPrefix}.startRun`)}</div>
</Button>
</div>
</div>
</div>
</PanelWrap>
)
}
export default React.memo(BeforeRunForm)

+ 41
- 0
web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx View File

@@ -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<Props> = ({
nodeName,
onHide,
children,
}) => {
const { t } = useTranslation()
return (
<div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt'>
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
<div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'>
<div className='truncate text-base font-semibold text-text-primary'>
{t(`${i18nPrefix}.testRun`)} {nodeName}
</div>
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => {
onHide()
}}>
<RiCloseLine className='h-4 w-4 text-text-tertiary ' />
</div>
</div>
{children}
</div>
</div>
)
}
export default React.memo(PanelWrap)

+ 12
- 9
web/app/components/workflow/nodes/_base/components/node-control.tsx View File

@@ -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<NodeControlProps> = ({
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 (
<div
className={`
@@ -49,23 +50,25 @@ const NodeControl: FC<NodeControlProps> = ({
onClick={e => e.stopPropagation()}
>
{
canRunBySingle(data.type) && (
canRunBySingle(data.type, isChildNode) && (
<div
className='flex h-5 w-5 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
const nextData: Record<string, any> = {
_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
? <Stop className='h-3 w-3' />
: (
<Tooltip

+ 4
- 2
web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx View File

@@ -83,14 +83,16 @@ const PanelOperatorPopup = ({

const link = useNodeHelpLink(data.type)

const isChildNode = !!(data.isInIteration || data.isInLoop)

return (
<div className='w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'>
{
(showChangeBlock || canRunBySingle(data.type)) && (
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
<>
<div className='p-1'>
{
canRunBySingle(data.type) && (
canRunBySingle(data.type, isChildNode) && (
<div
className={`
flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary

+ 429
- 0
web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx View File

@@ -0,0 +1,429 @@
import type {
FC,
ReactNode,
} from 'react'
import {
cloneElement,
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import {
RiCloseLine,
RiPlayLargeLine,
} from '@remixicon/react'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import NextStep from '../next-step'
import PanelOperator from '../panel-operator'
import NodePosition from '@/app/components/workflow/nodes/_base/components/node-position'
import HelpLink from '../help-link'
import {
DescriptionInput,
TitleInput,
} from '../title-description-input'
import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
import RetryOnPanel from '../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,
useToolIcon,
useWorkflowHistory,
} from '@/app/components/workflow/hooks'
import {
canRunBySingle,
hasErrorHandleNode,
hasRetryNode,
} from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip'
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useStore } from '@/app/components/workflow/store'
import Tab, { TabType } from './tab'
import LastRun from './last-run'
import useLastRun from './last-run/use-last-run'
import BeforeRunForm from '../before-run-form'
import { debounce } from 'lodash-es'
import { NODES_EXTRA_DATA } from '@/app/components/workflow/constants'
import { useLogs } from '@/app/components/workflow/run/hooks'
import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'

type BasePanelProps = {
children: ReactNode
} & Node

const BasePanel: FC<BasePanelProps> = ({
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<typeof data>({
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 (
<div className={cn(
'relative mr-1 h-full',
)}>
<div
ref={containerRef}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${nodePanelWidth}px`,
}}
>
<PanelWrap
nodeName={data.title}
onHide={hideSingleRun}
>
<div className='h-0 grow overflow-y-auto pb-4'>
<SpecialResultPanel {...passedLogParams} />
</div>
</PanelWrap>
</div>
</div>
)
}

if (isShowSingleRun) {
return (
<div className={cn(
'relative mr-1 h-full',
)}>
<div
ref={containerRef}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${nodePanelWidth}px`,
}}
>
<BeforeRunForm
nodeName={data.title}
nodeType={data.type}
onHide={hideSingleRun}
onRun={handleRunWithParams}
{...singleRunParams!}
{...passedLogParams}
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
/>
</div>
</div>
)
}

return (
<div className={cn(
'relative mr-1 h-full',
showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
)}>
<div
ref={triggerRef}
className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'>
<div className='h-10 w-0.5 rounded-sm bg-state-base-handle hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid'></div>
</div>
<div
ref={containerRef}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${nodePanelWidth}px`,
}}
>
<div className='sticky top-0 z-10 shrink-0 border-b-[0.5px] border-divider-regular bg-components-panel-bg'>
<div className='flex items-center px-4 pb-1 pt-4'>
<BlockIcon
className='mr-1 shrink-0'
type={data.type}
toolIcon={toolIcon}
size='md'
/>
<TitleInput
value={data.title || ''}
onBlur={handleTitleBlur}
/>
<div className='flex shrink-0 items-center text-text-tertiary'>
{
isSupportSingleRun && !nodesReadOnly && (
<Tooltip
popupContent={t('workflow.panel.runThisStep')}
popupClassName='mr-1'
disabled={isSingleRunning}
>
<div
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
if(isSingleRunning) {
handleNodeDataUpdate({
id,
data: {
_isSingleRun: false,
_singleRunningStatus: undefined,
},
})
}
else {
handleSingleRun()
}
}}
>
{
isSingleRunning ? <Stop className='h-4 w-4 text-text-tertiary' />
: <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
}
</div>
</Tooltip>
)
}
<NodePosition nodePosition={position} nodeWidth={width} nodeHeight={height}></NodePosition>
<HelpLink nodeType={data.type} />
<PanelOperator id={id} data={data} showHelpLink={false} />
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
<div
className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={() => handleNodeSelect(id, true)}
>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
<div className='p-2'>
<DescriptionInput
value={data.desc || ''}
onChange={handleDescriptionChange}
/>
</div>
<div className='pl-4'>
<Tab
value={tabType}
onChange={setTabType}
/>
</div>
<Split />
</div>

{tabType === TabType.settings && (
<>
<div>
{cloneElement(children as any, {
id,
data,
panelProps: {
getInputVars,
toVarInputs,
runInputData,
setRunInputData,
runResult,
runInputDataRef,
},
})}
</div>
<Split />
{
hasRetryNode(data.type) && (
<RetryOnPanel
id={id}
data={data}
/>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel
id={id}
data={data}
/>
)
}
{
!!availableNextBlocks.length && (
<div className='border-t-[0.5px] border-divider-regular p-4'>
<div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
{t('workflow.panel.nextStep').toLocaleUpperCase()}
</div>
<div className='system-xs-regular mb-2 text-text-tertiary'>
{t('workflow.panel.addNextStep')}
</div>
<NextStep selectedNode={{ id, data } as Node} />
</div>
)
}
</>
)}

{tabType === TabType.lastRun && (
<LastRun
appId={appDetail?.id || ''}
nodeId={id}
canSingleRun={isSupportSingleRun}
runningStatus={runningStatus}
isRunAfterSingleRun={isRunAfterSingleRun}
updateNodeRunningStatus={updateNodeRunningStatus}
onSingleRunClicked={handleSingleRun}
nodeInfo={nodeInfo}
singleRunResult={runResult!}
isPaused={isPaused}
{...passedLogParams}
/>
)}
</div>
</div>
)
}

export default memo(BasePanel)

+ 126
- 0
web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx View File

@@ -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<ResultPanelProps>

const LastRun: FC<Props> = ({
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<NodeRunningStatus | null>(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 (
<div className='flex h-0 grow flex-col items-center justify-center'>
<RiLoader2Line className='size-4 animate-spin text-text-tertiary' />
</div>)
}

if (isRunning)
return <ResultPanel status='running' showSteps={false} />

if (!isPaused && (noLastRun || !runResult)) {
return (
<NoData canSingleRun={canSingleRun} onSingleRun={onSingleRunClicked} />
)
}
return (
<div>
<ResultPanel
{...runResult as any}
{...otherResultPanelProps}
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens}
created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by}
nodeInfo={nodeInfo}
showSteps={false}
/>
</div>
)
}
export default React.memo(LastRun)

+ 36
- 0
web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx View File

@@ -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<Props> = ({
canSingleRun,
onSingleRun,
}) => {
const { t } = useTranslation()
return (
<div className='flex h-0 grow flex-col items-center justify-center'>
<ClockPlay className='h-8 w-8 text-text-quaternary' />
<div className='system-xs-regular my-2 text-text-tertiary'>{t('workflow.debug.noData.description')}</div>
{canSingleRun && (
<Button
className='flex'
size='small'
onClick={onSingleRun}
>
<RiPlayLine className='mr-1 h-3.5 w-3.5' />
<div>{t('workflow.debug.noData.runThisNode')}</div>
</Button>
)}
</div>
)
}
export default React.memo(NoData)

+ 330
- 0
web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts View File

@@ -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, any> = {
[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, any> = {
[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 = <T>(nodeType: BlockEnum) => {
return (id: string, payload: CommonNodeType<T>) => {
return getDataForCheckMoreHooks[nodeType]?.({ id, payload }) || {
getData: () => {
return {}
},
}
}
}

type Params<T> = Omit<OneStepRunParams<T>, 'isRunAfterSingleRun'>
const useLastRun = <T>({
...oneStepRunParams
}: Params<T>) => {
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<T>(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<string, any>) => {
if(!isIterationNode && !isLoopNode)
return data

const allVarObject = singleRunParams?.allVarObject || {}
const formattedData: Record<string, any> = {}
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<string, any>, 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<TabType>(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<string, any>) => {
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<string, boolean> = {}
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

+ 34
- 0
web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx View File

@@ -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<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
return (
<TabHeader
items={[
{ id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() },
{ id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() },
]}
itemClassName='ml-0'
value={value}
onChange={onChange as any}
/>
)
}
export default React.memo(Tab)

+ 143
- 28
web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts View File

@@ -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, Function> = {
[BlockEnum.LLM]: checkLLMValid,
@@ -66,13 +70,15 @@ const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.Loop]: checkLoopValid,
} as any

type Params<T> = {
export type Params<T> = {
id: string
data: CommonNodeType<T>
defaultRunInputData: Record<string, any>
moreDataForCheckValid?: any
iteratorInputKey?: string
loopInputKey?: string
isRunAfterSingleRun: boolean
isPaused: boolean
}

const varTypeToInputVarType = (type: VarType, {
@@ -105,6 +111,8 @@ const useOneStepRun = <T>({
moreDataForCheckValid,
iteratorInputKey,
loopInputKey,
isRunAfterSingleRun,
isPaused,
}: Params<T>) => {
const { t } = useTranslation()
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
@@ -112,6 +120,7 @@ const useOneStepRun = <T>({
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 = <T>({
}

const checkValid = checkValidFns[data.type]

const appId = useAppStore.getState().appDetail?.id
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
const runInputDataRef = useRef(runInputData)
@@ -150,26 +160,68 @@ const useOneStepRun = <T>({
runInputDataRef.current = data
setRunInputData(data)
}, [])
const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey].length : 0
const loopTimes = loopInputKey ? runInputData[loopInputKey].length : 0
const [runResult, setRunResult] = useState<any>(null)

const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
const isShowSingleRun = data._isSingleRun && canShowSingleRun
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([])
const [loopRunResult, setLoopRunResult] = useState<NodeTracing[]>([])
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<NodeRunResult | null>(null)
const {
appendNodeInspectVars,
invalidateSysVarValues,
invalidateConversationVarValues,
} = useInspectVarsCrud()
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
const isPausedRef = useRef(isPaused)
useEffect(() => {
if (!checkValid) {
setCanShowSingleRun(true)
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
}

if (data._isSingleRun) {
const { isValid, errorMessage } = checkValid(data, t, moreDataForCheckValid)
setCanShowSingleRun(isValid)
if (!isValid) {
// 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: {
@@ -179,17 +231,32 @@ const useOneStepRun = <T>({
})
Toast.notify({
type: 'error',
message: errorMessage,
message: res.errorMessage,
})
}
}
return res
}
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
const isShowSingleRun = data._isSingleRun && canShowSingleRun
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([])
const [loopRunResult, setLoopRunResult] = useState<NodeTracing[]>([])

useEffect(() => {
if (!checkValid) {
setCanShowSingleRun(true)
return
}

if (data._isSingleRun) {
const { isValid } = checkValidWrap()
setCanShowSingleRun(isValid)
}
// 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 = <T>({
},
})
}
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed

const handleRun = async (submitData: Record<string, any>) => {
@@ -217,13 +283,29 @@ const useOneStepRun = <T>({
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<string, any> = {}
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 = <T>({
{
onWorkflowStarted: noop,
onWorkflowFinished: (params) => {
if(isPausedRef.current)
return
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Succeeded,
},
})
@@ -311,10 +396,13 @@ const useOneStepRun = <T>({
setIterationRunResult(newIterationRunResult)
},
onError: () => {
if(isPausedRef.current)
return
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
@@ -332,10 +420,13 @@ const useOneStepRun = <T>({
{
onWorkflowStarted: noop,
onWorkflowFinished: (params) => {
if(isPausedRef.current)
return
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Succeeded,
},
})
@@ -409,10 +500,13 @@ const useOneStepRun = <T>({
setLoopRunResult(newLoopRunResult)
},
onError: () => {
if(isPausedRef.current)
return
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
@@ -425,11 +519,16 @@ const useOneStepRun = <T>({
}
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 = <T>({
}
}
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 = <T>({
})
}
}
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 = <T>({
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 = <T>({
runResult,
iterationRunResult,
loopRunResult,
setNodeRunning,
checkValid: checkValidWrap,
}
}


+ 47
- 6
web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts View File

@@ -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<T> = {
id: string
@@ -34,8 +35,27 @@ function useOutputVarList<T>({
outputKeyOrders = [],
onOutputKeyOrdersChange,
}: Params<T>) {
const {
renameInspectVarName,
deleteInspectVar,
nodesWithInspectVars,
} = useInspectVarsCrud()

const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()

// record the first old name value
const oldNameRecord = useRef<Record<string, string>>({})

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<T>({
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<T>({
}] = useBoolean(false)
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
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<T>({
})
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,

+ 8
- 4
web/app/components/workflow/nodes/_base/node.tsx View File

@@ -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<BaseNodeProps> = ({
}
}, [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<BaseNodeProps> = ({
} = 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<BaseNodeProps> = ({
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
}
{
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
isLoading && (
<RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
)
}
{
data._runningStatus === NodeRunningStatus.Succeeded && (
(!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)) && (
<RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
)
}

+ 0
- 214
web/app/components/workflow/nodes/_base/panel.tsx View File

@@ -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<BasePanelProps> = ({
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 (
<div className={cn(
'relative mr-2 h-full',
showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
)}>
<div
ref={triggerRef}
className='absolute -left-2 top-1/2 h-6 w-3 -translate-y-1/2 cursor-col-resize resize-x'>
<div className='h-6 w-1 rounded-sm bg-divider-regular'></div>
</div>
<div
ref={containerRef}
className={cn('h-full rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${panelWidth}px`,
}}
>
<div className='sticky top-0 z-10 border-b-[0.5px] border-divider-regular bg-components-panel-bg'>
<div className='flex items-center px-4 pb-1 pt-4'>
<BlockIcon
className='mr-1 shrink-0'
type={data.type}
toolIcon={toolIcon}
size='md'
/>
<TitleInput
value={data.title || ''}
onBlur={handleTitleBlur}
/>
<div className='flex shrink-0 items-center text-text-tertiary'>
{
canRunBySingle(data.type) && !nodesReadOnly && (
<Tooltip
popupContent={t('workflow.panel.runThisStep')}
popupClassName='mr-1'
>
<div
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
handleSyncWorkflowDraft(true)
}}
>
<RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
</div>
</Tooltip>
)
}
<NodePosition nodePosition={position} nodeWidth={width} nodeHeight={height}></NodePosition>
<HelpLink nodeType={data.type} />
<PanelOperator id={id} data={data} showHelpLink={false} />
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
<div
className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={() => handleNodeSelect(id, true)}
>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
<div className='p-2'>
<DescriptionInput
value={data.desc || ''}
onChange={handleDescriptionChange}
/>
</div>
</div>
<div>
{cloneElement(children as any, { id, data })}
</div>
<Split />
{
hasRetryNode(data.type) && (
<RetryOnPanel
id={id}
data={data}
/>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel
id={id}
data={data}
/>
)
}
{
!!availableNextBlocks.length && (
<div className='border-t-[0.5px] border-divider-regular p-4'>
<div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
{t('workflow.panel.nextStep').toLocaleUpperCase()}
</div>
<div className='system-xs-regular mb-2 text-text-tertiary'>
{t('workflow.panel.addNextStep')}
</div>
<NextStep selectedNode={{ id, data } as Node} />
</div>
)
}
</div>
</div>
)
}

export default memo(BasePanel)

+ 1
- 53
web/app/components/workflow/nodes/agent/panel.tsx View File

@@ -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<NodePanelProps<AgentNodeType>> = (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<NodePanelProps<AgentNodeType>> = (props) => {
))}
</OutputVars>
</div>
{
isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={<ResultPanel {...runResult} nodeInfo={nodeInfo} showSteps={false} {...logsParams} />}
/>
)
}
</div>
}


+ 0
- 42
web/app/components/workflow/nodes/agent/use-config.ts View File

@@ -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<AgentNodeType>({
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,

+ 90
- 0
web/app/components/workflow/nodes/agent/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
runResult: NodeTracing
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
getInputVars,
setRunInputData,
runResult,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<AgentNodeType>(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

+ 1
- 0
web/app/components/workflow/nodes/assigner/components/var-list/index.tsx View File

@@ -52,6 +52,7 @@ const VarList: FC<Props> = ({
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)

+ 2
- 0
web/app/components/workflow/nodes/assigner/types.ts View File

@@ -30,3 +30,5 @@ export type AssignerNodeType = CommonNodeType & {
version?: '1' | '2'
items: AssignerNodeOperation[]
}

export const writeModeTypesNum = [WriteMode.increment, WriteMode.decrement, WriteMode.multiply, WriteMode.divide]

+ 1
- 1
web/app/components/workflow/nodes/assigner/use-config.ts View File

@@ -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

+ 55
- 0
web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
setRunInputData,
varSelectorsToVarInputs,
}: Params) => {
const { inputs } = useNodeCrud<AssignerNodeType>(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

+ 0
- 31
web/app/components/workflow/nodes/code/panel.tsx View File

@@ -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<NodePanelProps<CodeNodeType>> = ({
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<NodePanelProps<CodeNodeType>> = ({
/>
</Field>
</div>
{
isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)
}
<RemoveEffectVarConfirm
isShow={isShowRemoveVarConfirm}
onCancel={hideRemoveVarConfirm}

+ 1
- 45
web/app/components/workflow/nodes/code/use-config.ts View File

@@ -8,7 +8,6 @@ import { useStore } from '../../store'
import type { CodeNodeType, OutputVar } from './types'
import { CodeLanguage } 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 { fetchNodeDefault } from '@/service/workflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
@@ -61,7 +60,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
})
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<CodeNodeType>({
id,
data: inputs,
defaultRunInputData: {},
})

const varInputs = toVarInputs(inputs.variables)

const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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,
}
}

+ 65
- 0
web/app/components/workflow/nodes/code/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
toVarInputs,
setRunInputData,
}: Params) => {
const { inputs } = useNodeCrud<CodeNodeType>(id, payload)

const varInputs = toVarInputs(inputs.variables)
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
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

+ 1
- 36
web/app/components/workflow/nodes/document-extractor/panel.tsx View File

@@ -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<NodePanelProps<DocExtractorNodeType>> = ({
inputs,
handleVarChanges,
filterVar,
// single run
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
files,
setFiles,
} = useConfig(id, data)

return (
@@ -93,30 +82,6 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
/>
</OutputVars>
</div>
{
isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: [{
label: t(`${i18nPrefix}.inputVar`)!,
variable: 'files',
type: InputVarType.multiFiles,
required: true,
}],
values: { files },
onChange: keyValue => setFiles(keyValue.files),
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)
}
</div>
)
}

+ 1
- 45
web/app/components/workflow/nodes/document-extractor/use-config.ts View File

@@ -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<DocExtractorNodeType>({
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,
}
}


+ 64
- 0
web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => 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<string, any>) => 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

+ 0
- 30
web/app/components/workflow/nodes/http/panel.tsx View File

@@ -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<NodePanelProps<HttpNodeType>> = ({
hideAuthorization,
setAuthorization,
setTimeout,
// single run
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
varInputs,
inputVarValues,
setInputVarValues,
runResult,
isShowCurlPanel,
showCurlPanel,
hideCurlPanel,
@@ -180,24 +168,6 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
</>
</OutputVars>
</div>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={[
{
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
{(isShowCurlPanel && !readOnly) && (
<CurlPanel
nodeId={id}

+ 1
- 61
web/app/components/workflow/nodes/http/use-config.ts View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import useVarList from '../_base/hooks/use-var-list'
@@ -9,7 +9,6 @@ import { type Authorization, type Body, BodyType, type HttpNodeType, type Method
import useKeyValueList from './hooks/use-key-value-list'
import { transformToBodyPayload } from './utils'
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'
@@ -125,55 +124,6 @@ const useConfig = (id: string, payload: HttpNodeType) => {
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
}, [])

// single run
const {
isShowSingleRun,
hideSingleRun,
getInputVars,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<HttpNodeType>({
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<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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,

+ 74
- 0
web/app/components/workflow/nodes/http/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
getInputVars,
setRunInputData,
}: Params) => {
const { inputs } = useNodeCrud<HttpNodeType>(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<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
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

+ 1
- 1
web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx View File

@@ -69,7 +69,7 @@ const ConditionOperator = ({
<RiArrowDownSLine className='ml-1 h-3.5 w-3.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<PortalToFollowElemContent className='z-[11]'>
<div className='rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
{
options.map(option => (

+ 166
- 0
web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[]
}
const useSingleRunFormParams = ({
payload,
runInputData,
setRunInputData,
getInputVars,
varSelectorsToVarInputs,
}: Params) => {
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
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<string, boolean> = {}
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

+ 2
- 2
web/app/components/workflow/nodes/index.tsx View File

@@ -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 (
<>
<BaseNode { ...props }>
<BaseNode {...props}>
<NodeComponent />
</BaseNode>
</>

+ 1
- 54
web/app/components/workflow/nodes/iteration/panel.tsx View File

@@ -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<NodePanelProps<IterationNodeType>> = ({
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 (
<div className='pb-2 pt-2'>
<div className='space-y-4 px-4 pb-4'>
@@ -137,38 +116,6 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false} />
</Field>
</div>

{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: [...usedOutVars],
values: inputVarValues,
onChange: setInputVarValues,
},
{
label: t(`${i18nPrefix}.input`)!,
inputs: [{
label: '',
variable: iteratorInputKey,
type: InputVarType.iterator,
required: false,
}],
values: { [iteratorInputKey]: iterator },
onChange: keyValue => setIterator(keyValue[iteratorInputKey]),
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={
<ResultPanel {...runResult} showSteps={false} nodeInfo={nodeInfo} {...logsParams} />
}
/>
)}
</div>
)
}

+ 1
- 0
web/app/components/workflow/nodes/iteration/types.ts View File

@@ -11,6 +11,7 @@ export type IterationNodeType = CommonNodeType & {
start_node_id: string // start node id in the iteration
iteration_id?: string
iterator_selector: ValueSelector
iterator_input_type: VarType
output_selector: ValueSelector
output_type: VarType // output type.
is_parallel: boolean // open the parallel mode or not

+ 14
- 157
web/app/components/workflow/nodes/iteration/use-config.ts View File

@@ -1,25 +1,25 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import {
useIsChatMode,
useIsNodeInIteration,
useNodesReadOnly,
useWorkflow,
} from '../../hooks'
import { VarType } from '../../types'
import type { ErrorHandleMode, ValueSelector, 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 type { IterationNodeType } from './types'
import { toNodeOutputVars } from '../_base/components/variable/utils'
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { Item } from '@/app/components/base/select'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { isEqual } from 'lodash-es'

const DELIMITER = '@@@@@'
const useConfig = (id: string, payload: IterationNodeType) => {
const {
deleteNodeInspectorVars,
} = useInspectVarsCrud()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { isNodeInIteration } = useIsNodeInIteration(id)
const isChatMode = useIsChatMode()

const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
@@ -28,21 +28,23 @@ const useConfig = (id: string, payload: IterationNodeType) => {
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
}, [])

const handleInputChange = useCallback((input: ValueSelector | string) => {
const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
const newInputs = produce(inputs, (draft) => {
draft.iterator_selector = input as ValueSelector || []
draft.iterator_input_type = varInfo?.type || VarType.arrayString
})
setInputs(newInputs)
}, [inputs, setInputs])

// output
const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
const beforeNodes = getBeforeNodesInSameBranch(id)
const { getIterationNodeChildren } = useWorkflow()
const iterationChildrenNodes = getIterationNodeChildren(id)
const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes]
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode)

const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
if (isEqual(inputs.output_selector, output as ValueSelector))
return

const newInputs = produce(inputs, (draft) => {
draft.output_selector = output as ValueSelector || []
const outputItemType = varInfo?.type || VarType.string
@@ -61,135 +63,8 @@ const useConfig = (id: string, payload: IterationNodeType) => {
} as Record<VarType, VarType>)[outputItemType] || VarType.arrayString
})
setInputs(newInputs)
}, [inputs, setInputs])

// single run
const iteratorInputKey = `${id}.input_selector`
const {
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun: doHandleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
iterationRunResult,
} = useOneStepRun<IterationNodeType>({
id,
data: inputs,
iteratorInputKey,
defaultRunInputData: {
[iteratorInputKey]: [''],
},
})

const [isShowIterationDetail, {
setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail,
}] = useBoolean(false)

const hideIterationDetail = useCallback(() => {
hideSingleRun()
doHideIterationDetail()
}, [doHideIterationDetail, hideSingleRun])

const showIterationDetail = useCallback(() => {
doShowIterationDetail()
}, [doShowIterationDetail])

const backToSingleRun = useCallback(() => {
hideIterationDetail()
showSingleRun()
}, [hideIterationDetail, showSingleRun])

const { usedOutVars, allVarObject } = (() => {
const vars: ValueSelector[] = []
const varObjs: Record<string, boolean> = {}
const allVarObject: Record<string, {
inSingleRunPassedKey: string
}> = {}
iterationChildrenNodes.forEach((node) => {
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
nodeVars.forEach((varSelector) => {
if (varSelector[0] === id) { // skip iteration node itself variable: item, index
return
}
const isInIteration = isNodeInIteration(varSelector[0])
if (isInIteration) // not pass iteration 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<string, any>) => {
const formattedData: Record<string, any> = {}
Object.keys(allVarObject).forEach((key) => {
const [varSectorStr, nodeId] = key.split(DELIMITER)
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
})
formattedData[iteratorInputKey] = data[iteratorInputKey]
doHandleRun(formattedData)
}, [allVarObject, doHandleRun, iteratorInputKey])

const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.filter(key => ![iteratorInputKey].includes(key))
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
const newVars = {
...newPayload,
[iteratorInputKey]: runInputData[iteratorInputKey],
}
setRunInputData(newVars)
}, [iteratorInputKey, runInputData, setRunInputData])

const iterator = runInputData[iteratorInputKey]
const setIterator = useCallback((newIterator: string[]) => {
setRunInputData({
...runInputData,
[iteratorInputKey]: newIterator,
})
}, [iteratorInputKey, runInputData, setRunInputData])
deleteNodeInspectorVars(id)
}, [deleteNodeInspectorVars, id, inputs, setInputs])

const changeParallel = useCallback((value: boolean) => {
const newInputs = produce(inputs, (draft) => {
@@ -218,24 +93,6 @@ const useConfig = (id: string, payload: IterationNodeType) => {
childrenNodeVars,
iterationChildrenNodes,
handleOutputVarChange,
isShowSingleRun,
showSingleRun,
hideSingleRun,
isShowIterationDetail,
showIterationDetail,
hideIterationDetail,
backToSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
inputVarValues,
setInputVarValues,
usedOutVars,
iterator,
setIterator,
iteratorInputKey,
iterationRunResult,
changeParallel,
changeErrorResponseMode,
changeParallelNums,

+ 154
- 0
web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts View File

@@ -0,0 +1,154 @@
import type { MutableRefObject } from 'react'
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import type { IterationNodeType } from './types'
import { useTranslation } from 'react-i18next'
import { useIsNodeInIteration, useWorkflow } from '../../hooks'
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils'
import { InputVarType, VarType } from '@/app/components/workflow/types'
import formatTracing from '@/app/components/workflow/run/utils/format-log'
import type { NodeTracing } from '@/types/workflow'
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'

const i18nPrefix = 'workflow.nodes.iteration'

type Params = {
id: string,
payload: IterationNodeType,
runInputData: Record<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
iterationRunResult: NodeTracing[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
toVarInputs,
setRunInputData,
iterationRunResult,
}: Params) => {
const { t } = useTranslation()
const { isNodeInIteration } = useIsNodeInIteration(id)

const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
const iterationChildrenNodes = getIterationNodeChildren(id)
const beforeNodes = getBeforeNodesInSameBranch(id)
const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes]

const iteratorInputKey = `${id}.input_selector`
const iterator = runInputData[iteratorInputKey]
const setIterator = useCallback((newIterator: string[]) => {
setRunInputData({
...runInputData,
[iteratorInputKey]: newIterator,
})
}, [iteratorInputKey, runInputData, setRunInputData])

const { usedOutVars, allVarObject } = (() => {
const vars: ValueSelector[] = []
const varObjs: Record<string, boolean> = {}
const allVarObject: Record<string, {
inSingleRunPassedKey: string
}> = {}
iterationChildrenNodes.forEach((node) => {
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
nodeVars.forEach((varSelector) => {
if (varSelector[0] === id) { // skip iteration node itself variable: item, index
return
}
const isInIteration = isNodeInIteration(varSelector[0])
if (isInIteration) // not pass iteration 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 setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const forms = useMemo(() => {
return [
{
inputs: [...usedOutVars],
values: inputVarValues,
onChange: setInputVarValues,
},
{
label: t(`${i18nPrefix}.input`)!,
inputs: [{
label: '',
variable: iteratorInputKey,
type: InputVarType.iterator,
required: false,
getVarValueFromDependent: true,
isFileItem: payload.iterator_input_type === VarType.arrayFile,
}],
values: { [iteratorInputKey]: iterator },
onChange: (keyValue: Record<string, any>) => setIterator(keyValue[iteratorInputKey]),
},
]
}, [inputVarValues, iterator, iteratorInputKey, payload.iterator_input_type, setInputVarValues, setIterator, t, usedOutVars])

const nodeInfo = formatTracing(iterationRunResult, t)[0]

const getDependentVars = () => {
return [payload.iterator_selector]
}
const getDependentVar = (variable: string) => {
if(variable === iteratorInputKey)
return payload.iterator_selector
}

return {
forms,
nodeInfo,
allVarObject,
getDependentVars,
getDependentVar,
}
}

export default useSingleRunFormParams

+ 1
- 33
web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx View File

@@ -16,9 +16,7 @@ import type { KnowledgeRetrievalNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
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'

const i18nPrefix = 'workflow.nodes.knowledgeRetrieval'

@@ -40,14 +38,6 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
selectedDatasets,
selectedDatasetsLoaded,
handleOnDatasetsChange,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
query,
setQuery,
runResult,
rerankModelOpen,
setRerankModelOpen,
handleAddCondition,
@@ -191,28 +181,6 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({

</>
</OutputVars>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: [{
label: t(`${i18nPrefix}.queryVariable`)!,
variable: 'query',
type: InputVarType.paragraph,
required: true,
}],
values: { query },
onChange: keyValue => setQuery(keyValue.query),
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
</div>
)

+ 3
- 38
web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts View File

@@ -37,7 +37,6 @@ import { DATASET_DEFAULT } from '@/config'
import type { DataSet } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
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 { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
@@ -173,7 +172,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
}
})
setInputs(newInput)
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentProvider?.provider, currentModel, currentRerankModel, rerankDefaultModel])
const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([])
const [rerankModelOpen, setRerankModelOpen] = useState(false)
@@ -230,7 +229,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
setInputs(newInputs)
setSelectedDatasetsLoaded(true)
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

useEffect(() => {
@@ -242,7 +241,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
setInputs(produce(inputs, (draft) => {
draft.query_variable_selector = query_variable_selector
}))
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const handleOnDatasetsChange = useCallback((newDatasets: DataSet[]) => {
@@ -280,32 +279,6 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
return varPayload.type === VarType.string
}, [])

// single run
const {
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<KnowledgeRetrievalNodeType>({
id,
data: inputs,
defaultRunInputData: {
query: '',
},
})

const query = runInputData.query
const setQuery = useCallback((newQuery: string) => {
setRunInputData({
...runInputData,
query: newQuery,
})
}, [runInputData, setRunInputData])

const handleMetadataFilterModeChange = useCallback((newMode: MetadataFilteringModeEnum) => {
setInputs(produce(inputRef.current, (draft) => {
draft.metadata_filtering_mode = newMode
@@ -425,14 +398,6 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
selectedDatasets: selectedDatasets.filter(d => d.name),
selectedDatasetsLoaded,
handleOnDatasetsChange,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
query,
setQuery,
runResult,
rerankModelOpen,
setRerankModelOpen,
handleMetadataFilterModeChange,

+ 63
- 0
web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts View File

@@ -0,0 +1,63 @@
import type { MutableRefObject } from 'react'
import { useTranslation } from 'react-i18next'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import type { KnowledgeRetrievalNodeType } from './types'

const i18nPrefix = 'workflow.nodes.knowledgeRetrieval'

type Params = {
id: string,
payload: KnowledgeRetrievalNodeType
runInputData: Record<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
payload,
runInputData,
setRunInputData,
}: Params) => {
const { t } = useTranslation()
const query = runInputData.query
const setQuery = useCallback((newQuery: string) => {
setRunInputData({
...runInputData,
query: newQuery,
})
}, [runInputData, setRunInputData])

const forms = useMemo(() => {
return [
{
inputs: [{
label: t(`${i18nPrefix}.queryVariable`)!,
variable: 'query',
type: InputVarType.paragraph,
required: true,
}],
values: { query },
onChange: (keyValue: Record<string, any>) => setQuery(keyValue.query),
},
]
}, [query, setQuery, t])

const getDependentVars = () => {
return [payload.query_variable_selector]
}
const getDependentVar = (variable: string) => {
if(variable === 'query')
return payload.query_variable_selector
}

return {
forms,
getDependentVars,
getDependentVar,
}
}

export default useSingleRunFormParams

+ 35
- 26
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx View File

@@ -1,4 +1,4 @@
import React, { type FC, useCallback, useEffect, useRef } from 'react'
import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import classNames from '@/utils/classnames'
@@ -14,6 +14,7 @@ type CodeEditorProps = {
showFormatButton?: boolean
editorWrapperClassName?: string
readOnly?: boolean
hideTopMenu?: boolean
} & React.HTMLAttributes<HTMLDivElement>

const CodeEditor: FC<CodeEditorProps> = ({
@@ -22,12 +23,14 @@ const CodeEditor: FC<CodeEditorProps> = ({
showFormatButton = true,
editorWrapperClassName,
readOnly = false,
hideTopMenu = false,
className,
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
const monacoRef = useRef<any>(null)
const editorRef = useRef<any>(null)
const [isMounted, setIsMounted] = React.useState(false)
const containerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
@@ -63,6 +66,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
},
})
monaco.editor.setTheme('light-theme')
setIsMounted(true)
}, [])

const formatJsonContent = useCallback(() => {
@@ -75,6 +79,11 @@ const CodeEditor: FC<CodeEditorProps> = ({
onUpdate?.(value)
}, [onUpdate])

const editorTheme = useMemo(() => {
if (theme === Theme.light)
return 'light-theme'
return 'dark-theme'
}, [theme])
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
editorRef.current?.layout()
@@ -89,39 +98,39 @@ const CodeEditor: FC<CodeEditorProps> = ({
}, [])

return (
<div className={classNames('flex flex-col h-full bg-components-input-bg-normal overflow-hidden', className)}>
<div className='flex items-center justify-between pl-2 pr-1 pt-1'>
<div className='system-xs-semibold-uppercase py-0.5 text-text-secondary'>
<span className='px-1 py-0.5'>JSON</span>
</div>
<div className='flex items-center gap-x-0.5'>
{showFormatButton && (
<Tooltip popupContent={t('common.operation.format')}>
<div className={classNames('flex flex-col h-full bg-components-input-bg-normal overflow-hidden', hideTopMenu && 'pt-2', className)}>
{!hideTopMenu && (
<div className='flex items-center justify-between pl-2 pr-1 pt-1'>
<div className='system-xs-semibold-uppercase py-0.5 text-text-secondary'>
<span className='px-1 py-0.5'>JSON</span>
</div>
<div className='flex items-center gap-x-0.5'>
{showFormatButton && (
<Tooltip popupContent={t('common.operation.format')}>
<button
type='button'
className='flex h-6 w-6 items-center justify-center'
onClick={formatJsonContent}
>
<RiIndentIncrease className='h-4 w-4 text-text-tertiary' />
</button>
</Tooltip>
)}
<Tooltip popupContent={t('common.operation.copy')}>
<button
type='button'
className='flex h-6 w-6 items-center justify-center'
onClick={formatJsonContent}
>
<RiIndentIncrease className='h-4 w-4 text-text-tertiary' />
onClick={() => copy(value)}>
<RiClipboardLine className='h-4 w-4 text-text-tertiary' />
</button>
</Tooltip>
)}
<Tooltip popupContent={t('common.operation.copy')}>
<button
type='button'
className='flex h-6 w-6 items-center justify-center'
onClick={() => copy(value)}>
<RiClipboardLine className='h-4 w-4 text-text-tertiary' />
</button>
</Tooltip>
</div>
</div>
</div>
<div
ref={containerRef}
className={classNames('relative overflow-hidden', editorWrapperClassName)}
>
)}
<div className={classNames('relative overflow-hidden', editorWrapperClassName)}>
<Editor
defaultLanguage='json'
theme={isMounted ? editorTheme : 'default-theme'} // sometimes not load the default theme
value={value}
onChange={handleEditorChange}
onMount={handleEditorDidMount}

+ 10
- 1
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx View File

@@ -1,21 +1,30 @@
import React, { type FC } from 'react'
import CodeEditor from './code-editor'
import cn from '@/utils/classnames'

type SchemaEditorProps = {
schema: string
onUpdate: (schema: string) => void
hideTopMenu?: boolean
className?: string
readonly?: boolean
}

const SchemaEditor: FC<SchemaEditorProps> = ({
schema,
onUpdate,
hideTopMenu,
className,
readonly = false,
}) => {
return (
<CodeEditor
className='grow rounded-xl'
readOnly={readonly}
className={cn('grow rounded-xl', className)}
editorWrapperClassName='grow'
value={schema}
onUpdate={onUpdate}
hideTopMenu={hideTopMenu}
/>
)
}

+ 25
- 0
web/app/components/workflow/nodes/llm/default.ts View File

@@ -1,8 +1,29 @@
// import { RETRIEVAL_OUTPUT_STRUCT } from '../../constants'
import { BlockEnum, EditionType } from '../../types'
import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
import type { LLMNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'

const RETRIEVAL_OUTPUT_STRUCT = `{
"content": "",
"title": "",
"url": "",
"icon": "",
"metadata": {
"dataset_id": "",
"dataset_name": "",
"document_id": [],
"document_name": "",
"document_data_source_type": "",
"segment_id": "",
"segment_position": "",
"segment_word_count": "",
"segment_hit_count": "",
"segment_index_node_hash": "",
"score": ""
}
}`

const i18nPrefix = 'workflow.errorMsg'

const nodeDefault: NodeDefault<LLMNodeType> = {
@@ -27,6 +48,10 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
enabled: false,
},
},
defaultRunInputData: {
'#context#': [RETRIEVAL_OUTPUT_STRUCT],
'#files#': [],
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS

+ 1
- 82
web/app/components/workflow/nodes/llm/panel.tsx View File

@@ -5,7 +5,6 @@ import MemoryConfig from '../_base/components/memory-config'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import ConfigVision from '../_base/components/config-vision'
import useConfig from './use-config'
import { findVariableWhenOnLLMVision } from '../utils'
import type { LLMNodeType } from './types'
import ConfigPrompt from './components/config-prompt'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
@@ -14,10 +13,7 @@ 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 BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Tooltip from '@/app/components/base/tooltip'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import StructureOutput from './components/structure-output'
@@ -31,7 +27,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
data,
}) => {
const { t } = useTranslation()

const {
readOnly,
inputs,
@@ -58,80 +53,16 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
handleMemoryChange,
handleVisionResolutionEnabledChange,
handleVisionResolutionChange,
isShowSingleRun,
hideSingleRun,
inputVarValues,
setInputVarValues,
visionFiles,
setVisionFiles,
contexts,
setContexts,
runningStatus,
isModelSupportStructuredOutput,
structuredOutputCollapsed,
setStructuredOutputCollapsed,
handleStructureOutputEnableChange,
handleStructureOutputChange,
handleRun,
handleStop,
varInputs,
runResult,
filterJinjia2InputVar,
} = useConfig(id, data)

const model = inputs.model

const singleRunForms = (() => {
const forms: FormProps[] = []

if (varInputs.length > 0) {
forms.push(
{
label: t(`${i18nPrefix}.singleRun.variable`)!,
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
)
}

if (inputs.context?.variable_selector && inputs.context?.variable_selector.length > 0) {
forms.push(
{
label: t(`${i18nPrefix}.context`)!,
inputs: [{
label: '',
variable: '#context#',
type: InputVarType.contexts,
required: false,
}],
values: { '#context#': contexts },
onChange: keyValue => setContexts(keyValue['#context#']),
},
)
}

if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) {
const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVars)

forms.push(
{
label: t(`${i18nPrefix}.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 handleModelChange = useCallback((model: {
provider: string
modelId: string
@@ -344,18 +275,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
)}
</>
</OutputVars>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
)
}

+ 7
- 92
web/app/components/workflow/nodes/llm/use-config.ts View File

@@ -16,9 +16,8 @@ import {
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
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 { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'

const useConfig = (id: string, payload: LLMNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@@ -29,6 +28,8 @@ const useConfig = (id: string, payload: LLMNodeType) => {
const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
const inputRef = useRef(inputs)

const { deleteNodeInspectorVars } = useInspectVarsCrud()

const setInputs = useCallback((newInputs: LLMNodeType) => {
if (newInputs.memory && !newInputs.memory.role_prefix) {
const newPayload = produce(newInputs, (draft) => {
@@ -293,14 +294,16 @@ const useConfig = (id: string, payload: LLMNodeType) => {
setInputs(newInputs)
if (enabled)
setStructuredOutputCollapsed(false)
}, [inputs, setInputs])
deleteNodeInspectorVars(id)
}, [inputs, setInputs, deleteNodeInspectorVars, id])

const handleStructureOutputChange = useCallback((newOutput: StructuredOutput) => {
const newInputs = produce(inputs, (draft) => {
draft.structured_output = newOutput
})
setInputs(newInputs)
}, [inputs, setInputs])
deleteNodeInspectorVars(id)
}, [inputs, setInputs, deleteNodeInspectorVars, id])

const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
@@ -322,81 +325,6 @@ const useConfig = (id: string, payload: LLMNodeType) => {
filterVar: filterMemoryPromptVar,
})

// single run
const {
isShowSingleRun,
hideSingleRun,
getInputVars,
runningStatus,
handleRun,
handleStop,
runInputData,
runInputDataRef,
setRunInputData,
runResult,
toVarInputs,
} = useOneStepRun<LLMNodeType>({
id,
data: inputs,
defaultRunInputData: {
'#context#': [RETRIEVAL_OUTPUT_STRUCT],
'#files#': [],
},
})

const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.filter(key => !['#context#', '#files#'].includes(key))
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
const newVars = {
...newPayload,
'#context#': runInputDataRef.current['#context#'],
'#files#': runInputDataRef.current['#files#'],
}
setRunInputData(newVars)
}, [runInputDataRef, setRunInputData])

const contexts = runInputData['#context#']
const setContexts = useCallback((newContexts: string[]) => {
setRunInputData({
...runInputDataRef.current,
'#context#': newContexts,
})
}, [runInputDataRef, setRunInputData])

const visionFiles = runInputData['#files#']
const setVisionFiles = useCallback((newFiles: any[]) => {
setRunInputData({
...runInputDataRef.current,
'#files#': newFiles,
})
}, [runInputDataRef, setRunInputData])

const allVarStrArr = (() => {
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
if (isChatMode && isChatModel && !!inputs.memory) {
arr.push('{{#sys.query#}}')
arr.push(inputs.memory.query_prompt_template)
}

return arr
})()

const varInputs = (() => {
const vars = getInputVars(allVarStrArr)
if (isShowVars)
return [...vars, ...toVarInputs(inputs.prompt_config?.jinja2_variables || [])]

return vars
})()

return {
readOnly,
isChatMode,
@@ -423,24 +351,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
handleSyeQueryChange,
handleVisionResolutionEnabledChange,
handleVisionResolutionChange,
isShowSingleRun,
hideSingleRun,
inputVarValues,
setInputVarValues,
visionFiles,
setVisionFiles,
contexts,
setContexts,
varInputs,
runningStatus,
isModelSupportStructuredOutput,
handleStructureOutputChange,
structuredOutputCollapsed,
setStructuredOutputCollapsed,
handleStructureOutputEnableChange,
handleRun,
handleStop,
runResult,
filterJinjia2InputVar,
}
}

+ 198
- 0
web/app/components/workflow/nodes/llm/use-single-run-form-params.ts View File

@@ -0,0 +1,198 @@
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, PromptItem, Var, Variable } from '@/app/components/workflow/types'
import { InputVarType, VarType } from '@/app/components/workflow/types'
import type { LLMNodeType } from './types'
import { EditionType } from '../../types'
import useNodeCrud from '../_base/hooks/use-node-crud'
import { useIsChatMode } from '../../hooks'
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.llm'
type Params = {
id: string,
payload: LLMNodeType,
runInputData: Record<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
runInputDataRef,
getInputVars,
setRunInputData,
toVarInputs,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<LLMNodeType>(id, payload)
const getVarInputs = getInputVars
const isChatMode = useIsChatMode()

const contexts = runInputData['#context#']
const setContexts = useCallback((newContexts: string[]) => {
setRunInputData?.({
...runInputDataRef.current,
'#context#': newContexts,
})
}, [runInputDataRef, setRunInputData])

const visionFiles = runInputData['#files#']
const setVisionFiles = useCallback((newFiles: any[]) => {
setRunInputData?.({
...runInputDataRef.current,
'#files#': newFiles,
})
}, [runInputDataRef, setRunInputData])

// model
const model = inputs.model
const modelMode = inputs.model?.mode
const isChatModel = modelMode === 'chat'
const {
isVisionModel,
} = useConfigVision(model, {
payload: inputs.vision,
onChange: noop,
})

const isShowVars = (() => {
if (isChatModel)
return (inputs.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)

return (inputs.prompt_template as PromptItem).edition_type === EditionType.jinja2
})()

const filterMemoryPromptVar = useCallback((varPayload: Var) => {
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, [])

const {
availableVars,
} = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: filterMemoryPromptVar,
})

const allVarStrArr = (() => {
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
if (isChatMode && isChatModel && !!inputs.memory) {
arr.push('{{#sys.query#}}')
arr.push(inputs.memory.query_prompt_template)
}

return arr
})()
const varInputs = (() => {
const vars = getVarInputs(allVarStrArr) || []
if (isShowVars)
return [...vars, ...(toVarInputs ? (toVarInputs(inputs.prompt_config?.jinja2_variables || [])) : [])]

return vars
})()

const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.filter(key => !['#context#', '#files#'].includes(key))
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
const newVars = {
...newPayload,
'#context#': runInputDataRef.current['#context#'],
'#files#': runInputDataRef.current['#files#'],
}
setRunInputData?.(newVars)
}, [runInputDataRef, setRunInputData])

const forms = (() => {
const forms: FormProps[] = []

if (varInputs.length > 0) {
forms.push(
{
label: t(`${i18nPrefix}.singleRun.variable`)!,
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
)
}

if (inputs.context?.variable_selector && inputs.context?.variable_selector.length > 0) {
forms.push(
{
label: t(`${i18nPrefix}.context`)!,
inputs: [{
label: '',
variable: '#context#',
type: InputVarType.contexts,
required: false,
}],
values: { '#context#': contexts },
onChange: keyValue => setContexts(keyValue['#context#']),
},
)
}
if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) {
const currentVariable = findVariableWhenOnLLMVision(payload.vision.configs.variable_selector, availableVars)

forms.push(
{
label: t(`${i18nPrefix}.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 contextVar = payload.context.variable_selector
const vars = [...promptVars, contextVar]
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 === '#context#')
return payload.context.variable_selector

if(variable === '#files#')
return payload.vision.configs?.variable_selector

return false
}

return {
forms,
getDependentVars,
getDependentVar,
}
}

export default useSingleRunFormParams

+ 1
- 43
web/app/components/workflow/nodes/loop/panel.tsx View File

@@ -1,9 +1,8 @@
import type { FC } from 'react'
import React, { useMemo } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Split from '../_base/components/split'
import ResultPanel from '../../run/result-panel'
import InputNumberWithSlider from '../_base/components/input-number-with-slider'
import type { LoopNodeType } from './types'
import useConfig from './use-config'
@@ -11,10 +10,7 @@ import ConditionWrap from './components/condition-wrap'
import LoopVariable from './components/loop-variables'
import 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 formatTracing from '@/app/components/workflow/run/utils/format-log'

import { useLogs } from '@/app/components/workflow/run/hooks'
import { LOOP_NODE_MAX_COUNT } from '@/config'

const i18nPrefix = 'workflow.nodes.loop'
@@ -30,13 +26,6 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
inputs,
childrenNodeVars,
loopChildrenNodes,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
loopRunResult,
handleAddCondition,
handleUpdateCondition,
handleRemoveCondition,
@@ -51,23 +40,6 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
handleUpdateLoopVariable,
} = useConfig(id, data)

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 logsParams = useLogs()

return (
<div className='mt-2'>
<div>
@@ -139,20 +111,6 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
</Select>
</Field>
</div> */}
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={
<ResultPanel {...runResult} showSteps={false} nodeInfo={nodeInfo} {...logsParams} />
}
/>
)}
</div>
)
}

+ 2
- 150
web/app/components/workflow/nodes/loop/use-config.ts View File

@@ -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<LoopNodeType>({
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<string, boolean> = {}
const allVarObject: Record<string, {
inSingleRunPassedKey: string
}> = {}
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<string, any>) => {
const formattedData: Record<string, any> = {}
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<string, any> = {}
Object.keys(runInputData)
.filter(key => ![loopInputKey].includes(key))
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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,

+ 221
- 0
web/app/components/workflow/nodes/loop/use-single-run-form-params.ts View File

@@ -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<string, any>
runResult: NodeTracing
loopRunResult: NodeTracing[]
setRunInputData: (data: Record<string, any>) => 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<string, boolean> = {}
const allVarObject: Record<string, {
inSingleRunPassedKey: string
}> = {}
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<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])

const inputVarValues = (() => {
const vars: Record<string, any> = {}
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<string, boolean> = {}
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

+ 1
- 66
web/app/components/workflow/nodes/parameter-extractor/panel.tsx View File

@@ -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<NodePanelProps<ParameterExtractorNodeType>> = ({
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 (
<div className='pt-2'>
<div className='space-y-4 px-4'>
@@ -255,17 +201,6 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
</OutputVars>
</div>
</>)}
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
)
}

+ 15
- 71
web/app/components/workflow/nodes/parameter-extractor/use-config.ts View File

@@ -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<string, any>) => {
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<ParameterExtractorNodeType>({
id,
data: inputs,
defaultRunInputData: {
'query': '',
'#files#': [],
},
})

const varInputs = getInputVars([inputs.instruction])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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,
}
}


+ 148
- 0
web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
runInputDataRef,
getInputVars,
setRunInputData,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<ParameterExtractorNodeType>(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<string, any> = {}
Object.keys(runInputData)
.filter(key => !['#context#', '#files#'].includes(key))
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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

+ 1
- 66
web/app/components/workflow/nodes/question-classifier/panel.tsx View File

@@ -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<NodePanelProps<QuestionClassifierNodeType>> = ({
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 (
<div className='pt-2'>
<div className='space-y-4 px-4'>
@@ -186,17 +132,6 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
</>
</OutputVars>
</div>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
)
}

+ 2
- 69
web/app/components/workflow/nodes/question-classifier/use-config.ts View File

@@ -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<QuestionClassifierNodeType>({
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<string, any> = {
query,
}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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,
}
}


+ 146
- 0
web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
runInputDataRef,
getInputVars,
setRunInputData,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<QuestionClassifierNodeType>(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<string, any> = {}
Object.keys(runInputData)
.filter(key => !['#files#'].includes(key))
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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

+ 19
- 1
web/app/components/workflow/nodes/start/use-config.ts View File

@@ -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<StartNodeType>(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)
}
else if(moreInfo?.payload?.type !== ChangeType.remove) { // edit var type
deleteNodeInspectorVars(id)
}
}, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm])
}, [deleteInspectVar, deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, nodesWithInspectVars, renameInspectVarName, setInputs, showRemoveVarConfirm])

const removeVarInNode = useCallback(() => {
const newInputs = produce(inputs, (draft) => {

+ 87
- 0
web/app/components/workflow/nodes/start/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => 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

+ 0
- 29
web/app/components/workflow/nodes/template-transform/panel.tsx View File

@@ -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<NodePanelProps<TemplateTransformNodeType>> = ({
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<NodePanelProps<TemplateTransformNodeType>> = ({
</>
</OutputVars>
</div>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
)
}

+ 1
- 43
web/app/components/workflow/nodes/template-transform/use-config.ts View File

@@ -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<TemplateTransformNodeType>({
id,
data: inputs,
defaultRunInputData: {},
})
const varInputs = toVarInputs(inputs.variables)

const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()

const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
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,
}
}


+ 65
- 0
web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
toVarInputs,
setRunInputData,
}: Params) => {
const { inputs } = useNodeCrud<TemplateTransformNodeType>(id, payload)

const varInputs = toVarInputs(inputs.variables)
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
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

+ 1
- 35
web/app/components/workflow/nodes/tool/panel.tsx View File

@@ -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<NodePanelProps<ToolNodeType>> = ({
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 <div className='flex h-[200px] items-center justify-center'>
@@ -180,21 +161,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
</>
</OutputVars>
</div>

{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
toolIcon={toolIcon}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
{...logsParams}
result={<ResultPanel {...runResult} showSteps={false} {...logsParams} nodeInfo={nodeInfo} />}
/>
)}
</div>
)
}

+ 5
- 92
web/app/components/workflow/nodes/tool/use-config.ts View File

@@ -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<Record<string, any>>({})
const setInputVarValues = (value: Record<string, any>) => {
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<ToolNodeType>({
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<string, any>) => {
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,
}
}


+ 20
- 0
web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts View File

@@ -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

+ 94
- 0
web/app/components/workflow/nodes/tool/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
runResult: NodeTracing
}
const useSingleRunFormParams = ({
id,
payload,
getInputVars,
setRunInputData,
runResult,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<ToolNodeType>(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<Record<string, any>>({})
const setInputVarValues = useCallback((value: Record<string, any>) => {
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

+ 29
- 5
web/app/components/workflow/nodes/variable-assigner/use-config.ts View File

@@ -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<Record<string, string>>({})

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) => {

+ 92
- 0
web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts View File

@@ -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<string, any>
runInputDataRef: MutableRefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[]
}
const useSingleRunFormParams = ({
payload,
runInputData,
setRunInputData,
varSelectorsToVarInputs,
}: Params) => {
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
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<string, boolean> = {}
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

+ 1
- 1
web/app/components/workflow/operator/add-block.tsx View File

@@ -96,7 +96,7 @@ const AddBlock = ({
onOpenChange={handleOpenChange}
disabled={nodesReadOnly}
onSelect={handleSelect}
placement='top-start'
placement='right-start'
offset={offset ?? {
mainAxis: 4,
crossAxis: -8,

+ 21
- 3
web/app/components/workflow/operator/control.tsx View File

@@ -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<HTMLDivElement>) => {
if (getNodesReadOnly())
@@ -45,7 +50,7 @@ const Control = () => {
}

return (
<div className='flex items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'>
<div className='flex flex-col items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'>
<AddBlock />
<TipPopup title={t('workflow.nodes.note.addNote')}>
<div
@@ -58,7 +63,7 @@ const Control = () => {
<RiStickyNoteAddLine className='h-4 w-4' />
</div>
</TipPopup>
<Divider type='vertical' className='mx-0.5 h-3.5' />
<Divider className='my-1 w-3.5' />
<TipPopup title={t('workflow.common.pointerMode')} shortcuts={['v']}>
<div
className={cn(
@@ -83,7 +88,7 @@ const Control = () => {
<RiHand className='h-4 w-4' />
</div>
</TipPopup>
<Divider type='vertical' className='mx-0.5 h-3.5' />
<Divider className='my-1 w-3.5' />
<ExportImage />
<TipPopup title={t('workflow.panel.organizeBlocks')} shortcuts={['ctrl', 'o']}>
<div
@@ -96,6 +101,19 @@ const Control = () => {
<RiFunctionAddLine className='h-4 w-4' />
</div>
</TipPopup>
<TipPopup title={maximizeCanvas ? t('workflow.panel.minimize') : t('workflow.panel.maximize')} shortcuts={['f']}>
<div
className={cn(
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover hover:text-text-secondary',
maximizeCanvas ? 'bg-state-accent-active text-text-accent hover:text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
)}
onClick={handleToggleMaximizeCanvas}
>
{maximizeCanvas && <RiAspectRatioFill className='h-4 w-4' />}
{!maximizeCanvas && <RiAspectRatioLine className='h-4 w-4' />}
</div>
</TipPopup>
</div>
)
}

+ 60
- 18
web/app/components/workflow/operator/index.tsx View File

@@ -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<HTMLDivElement>(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 (
<>
<MiniMap
pannable
zoomable
style={{
width: 102,
height: 72,
}}
maskColor='var(--color-workflow-minimap-bg)'
className='!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px]
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
/>
<div className='absolute bottom-4 left-4 z-[9] mt-1 flex items-center gap-2'>
<ZoomInOut />
<div
ref={bottomPanelRef}
className='absolute bottom-0 left-0 right-0 z-10 px-1'
style={
{
width: bottomPanelWidth,
}
}
>
<div className='flex justify-between px-1 pb-2'>
<UndoRedo handleUndo={handleUndo} handleRedo={handleRedo} />
<Control />
<VariableTrigger />
<div className='relative'>
<MiniMap
pannable
zoomable
style={{
width: 102,
height: 72,
}}
maskColor='var(--color-workflow-minimap-bg)'
className='!absolute !bottom-10 z-[9] !m-0 !h-[73px] !w-[103px] !rounded-lg !border-[0.5px]
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
/>
<ZoomInOut />
</div>
</div>
</>
<VariableInspectPanel />
</div>
)
}


+ 1
- 1
web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx View File

@@ -377,7 +377,7 @@ const ChatVariableModal = ({
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.chatVariable.modal.description')}</div>
<div className='flex'>
<textarea
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
value={des}
placeholder={t('workflow.chatVariable.modal.descriptionPlaceholder') || ''}
onChange={e => setDes(e.target.value)}

+ 16
- 5
web/app/components/workflow/panel/chat-variable-panel/index.tsx View File

@@ -23,6 +23,7 @@ import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-syn
import { BlockEnum } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
import cn from '@/utils/classnames'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'

const ChatVariablePanel = () => {
const { t } = useTranslation()
@@ -32,6 +33,16 @@ const ChatVariablePanel = () => {
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
const updateChatVarList = useStore(s => s.setConversationVariables)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const {
invalidateConversationVarValues,
} = useInspectVarsCrud()
const handleVarChanged = useCallback(() => {
doSyncWorkflowDraft(false, {
onSuccess() {
invalidateConversationVarValues()
},
})
}, [doSyncWorkflowDraft, invalidateConversationVarValues])

const [showTip, setShowTip] = useState(true)
const [showVariableModal, setShowVariableModal] = useState(false)
@@ -71,8 +82,8 @@ const ChatVariablePanel = () => {
updateChatVarList(varList.filter(v => v.id !== chatVar.id))
setCacheForDelete(undefined)
setShowRemoveConfirm(false)
doSyncWorkflowDraft()
}, [doSyncWorkflowDraft, removeUsedVarInNodes, updateChatVarList, varList])
handleVarChanged()
}, [handleVarChanged, removeUsedVarInNodes, updateChatVarList, varList])

const deleteCheck = useCallback((chatVar: ConversationVariable) => {
const effectedNodes = getEffectedNodes(chatVar)
@@ -90,7 +101,7 @@ const ChatVariablePanel = () => {
if (!currentVar) {
const newList = [chatVar, ...varList]
updateChatVarList(newList)
doSyncWorkflowDraft()
handleVarChanged()
return
}
// edit chatVar
@@ -108,8 +119,8 @@ const ChatVariablePanel = () => {
})
setNodes(newNodes)
}
doSyncWorkflowDraft()
}, [currentVar, doSyncWorkflowDraft, getEffectedNodes, store, updateChatVarList, varList])
handleVarChanged()
}, [currentVar, getEffectedNodes, handleVarChanged, store, updateChatVarList, varList])

return (
<div

+ 8
- 0
web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx View File

@@ -21,6 +21,8 @@ import {
import { useStore as useAppStore } from '@/app/components/app/store'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'

type ChatWrapperProps = {
showConversationVariableModal: boolean
@@ -105,6 +107,12 @@ const ChatWrapper = (
)
}, [chatList, doSend])

const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {
if (v.type === EVENT_WORKFLOW_STOP)
handleStop()
})

useImperativeHandle(ref, () => {
return {
handleRestart,

+ 8
- 0
web/app/components/workflow/panel/debug-and-preview/hooks.ts View File

@@ -30,6 +30,9 @@ import {
} from '@/app/components/base/file-uploader/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { getThreadMessages } from '@/app/components/base/chat/utils'
import { useInvalidAllLastRun } from '@/service/use-workflow'
import { useParams } from 'next/navigation'
import useSetWorkflowVarsWithValue from '@/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars'

type GetAbortController = (abortController: AbortController) => void
type SendCallback = {
@@ -53,6 +56,9 @@ export const useChat = (
const taskIdRef = useRef('')
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
const { appId } = useParams()
const invalidAllLastRun = useInvalidAllLastRun(appId as string)
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const {
@@ -288,6 +294,8 @@ export const useChat = (
},
async onCompleted(hasError?: boolean, errorMessage?: string) {
handleResponding(false)
fetchInspectVars()
invalidAllLastRun()

if (hasError) {
if (errorMessage) {

+ 72
- 80
web/app/components/workflow/panel/debug-and-preview/index.tsx View File

@@ -1,7 +1,7 @@
import {
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
@@ -16,14 +16,14 @@ import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
import { BlockEnum } from '../../types'
import type { StartNodeType } from '../../nodes/start/types'
import { useResizePanel } from '../../nodes/_base/hooks/use-resize-panel'
import ChatWrapper from './chat-wrapper'
import cn from '@/utils/classnames'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import Tooltip from '@/app/components/base/tooltip'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { useStore } from '@/app/components/workflow/store'
import { noop } from 'lodash-es'
import { debounce, noop } from 'lodash-es'

export type ChatWrapperRefType = {
handleRestart: () => void
@@ -34,9 +34,9 @@ const DebugAndPreview = () => {
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
const varList = useStore(s => s.conversationVariables)
const [expanded, setExpanded] = useState(true)
const nodes = useNodes<StartNodeType>()
const selectedNode = nodes.find(node => node.data.selected)
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || []
const visibleVariables = variables.filter(v => v.hide !== true)
@@ -49,94 +49,86 @@ const DebugAndPreview = () => {
chatRef.current.handleRestart()
}

const [panelWidth, setPanelWidth] = useState(420)
const [isResizing, setIsResizing] = useState(false)
const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth)
const nodePanelWidth = useStore(s => s.nodePanelWidth)
const [panelWidth, setPanelWidth] = useState(400)
const handleResize = useCallback((width: number) => {
setPanelWidth(width)
}, [setPanelWidth])
const maxPanelWidth = useMemo(() => {
if (!workflowCanvasWidth)
return 720

const startResizing = useCallback((e: React.MouseEvent) => {
e.preventDefault()
setIsResizing(true)
}, [])
if (!selectedNode)
return workflowCanvasWidth - 400

const stopResizing = useCallback(() => {
setIsResizing(false)
}, [])

const resize = useCallback((e: MouseEvent) => {
if (isResizing) {
const newWidth = window.innerWidth - e.clientX
if (newWidth > 420 && newWidth < 1024)
setPanelWidth(newWidth)
}
}, [isResizing])

useEffect(() => {
window.addEventListener('mousemove', resize)
window.addEventListener('mouseup', stopResizing)
return () => {
window.removeEventListener('mousemove', resize)
window.removeEventListener('mouseup', stopResizing)
}
}, [resize, stopResizing])
return workflowCanvasWidth - 400 - 400
}, [workflowCanvasWidth, selectedNode, nodePanelWidth])
const {
triggerRef,
containerRef,
} = useResizePanel({
direction: 'horizontal',
triggerDirection: 'left',
minWidth: 400,
maxWidth: maxPanelWidth,
onResize: debounce(handleResize),
})

return (
<div
className={cn(
'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-chatbot-bg shadow-xl',
)}
style={{ width: `${panelWidth}px` }}
>
<div className='relative h-full'>
<div
ref={triggerRef}
className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'>
<div className='h-10 w-0.5 rounded-sm bg-state-base-handle hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid'></div>
</div>
<div
className="absolute bottom-0 left-[3px] top-1/2 z-50 h-6 w-[3px] cursor-col-resize rounded bg-gray-300"
onMouseDown={startResizing}
/>
<div className='system-xl-semibold flex shrink-0 items-center justify-between px-4 pb-2 pt-3 text-text-primary'>
<div className='h-8'>{t('workflow.common.debugAndPreview').toLocaleUpperCase()}</div>
<div className='flex items-center gap-1'>
<Tooltip
popupContent={t('common.operation.refresh')}
>
<ActionButton onClick={() => handleRestartChat()}>
<RefreshCcw01 className='h-4 w-4' />
</ActionButton>
</Tooltip>
{varList.length > 0 && (
ref={containerRef}
className={cn(
'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-chatbot-bg shadow-xl',
)}
style={{ width: `${panelWidth}px` }}
>
<div className='system-xl-semibold flex shrink-0 items-center justify-between px-4 pb-2 pt-3 text-text-primary'>
<div className='h-8'>{t('workflow.common.debugAndPreview').toLocaleUpperCase()}</div>
<div className='flex items-center gap-1'>
<Tooltip
popupContent={t('workflow.chatVariable.panelTitle')}
popupContent={t('common.operation.refresh')}
>
<ActionButton onClick={() => setShowConversationVariableModal(true)}>
<BubbleX className='h-4 w-4' />
<ActionButton onClick={() => handleRestartChat()}>
<RefreshCcw01 className='h-4 w-4' />
</ActionButton>
</Tooltip>
)}
{visibleVariables.length > 0 && (
<div className='relative'>
<Tooltip
popupContent={t('workflow.panel.userInputField')}
>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
</Tooltip>
{expanded && <div className='absolute bottom-[-17px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg' />}
{visibleVariables.length > 0 && (
<div className='relative'>
<Tooltip
popupContent={t('workflow.panel.userInputField')}
>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
</Tooltip>
{expanded && <div className='absolute bottom-[-17px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg' />}
</div>
)}
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular'></div>
<div
className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={handleCancelDebugAndPreviewPanel}
>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
)}
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular'></div>
<div
className='flex h-6 w-6 cursor-pointer items-center justify-center'
onClick={handleCancelDebugAndPreviewPanel}
>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
</div>
</div>
<div className='grow overflow-y-auto rounded-b-2xl'>
<ChatWrapper
ref={chatRef}
showConversationVariableModal={showConversationVariableModal}
onConversationModalHide={() => setShowConversationVariableModal(false)}
showInputsFieldsPanel={expanded}
onHide={() => setExpanded(false)}
/>
<div className='grow overflow-y-auto rounded-b-2xl'>
<ChatWrapper
ref={chatRef}
showConversationVariableModal={showConversationVariableModal}
onConversationModalHide={() => setShowConversationVariableModal(false)}
showInputsFieldsPanel={expanded}
onHide={() => setExpanded(false)}
/>
</div>
</div>
</div>
)

+ 53
- 10
web/app/components/workflow/panel/index.tsx View File

@@ -1,5 +1,5 @@
import type { FC } from 'react'
import { memo } from 'react'
import { memo, useEffect, useRef } from 'react'
import { useNodes } from 'reactflow'
import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
@@ -21,10 +21,48 @@ const Panel: FC<PanelProps> = ({
const showEnvPanel = useStore(s => s.showEnvPanel)
const isRestoring = useStore(s => s.isRestoring)

const rightPanelRef = useRef<HTMLDivElement>(null)
const setRightPanelWidth = useStore(s => s.setRightPanelWidth)

// get right panel width
useEffect(() => {
if (rightPanelRef.current) {
const resizeRightPanelObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize } = entry.borderBoxSize[0]
setRightPanelWidth(inlineSize)
}
})
resizeRightPanelObserver.observe(rightPanelRef.current)
return () => {
resizeRightPanelObserver.disconnect()
}
}
}, [setRightPanelWidth])

const otherPanelRef = useRef<HTMLDivElement>(null)
const setOtherPanelWidth = useStore(s => s.setOtherPanelWidth)

// get other panel width
useEffect(() => {
if (otherPanelRef.current) {
const resizeOtherPanelObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize } = entry.borderBoxSize[0]
setOtherPanelWidth(inlineSize)
}
})
resizeOtherPanelObserver.observe(otherPanelRef.current)
return () => {
resizeOtherPanelObserver.disconnect()
}
}
}, [setOtherPanelWidth])
return (
<div
ref={rightPanelRef}
tabIndex={-1}
className={cn('absolute bottom-2 right-0 top-14 z-10 flex outline-none')}
className={cn('absolute bottom-1 right-0 top-14 z-10 flex outline-none')}
key={`${isRestoring}`}
>
{
@@ -35,14 +73,19 @@ const Panel: FC<PanelProps> = ({
<NodePanel {...selectedNode!} />
)
}
{
components?.right
}
{
showEnvPanel && (
<EnvPanel />
)
}
<div
className='relative'
ref={otherPanelRef}
>
{
components?.right
}
{
showEnvPanel && (
<EnvPanel />
)
}
</div>
</div>
)
}

+ 11
- 3
web/app/components/workflow/panel/version-history-panel/index.tsx View File

@@ -9,7 +9,7 @@ import VersionHistoryItem from './version-history-item'
import Filter from './filter'
import type { VersionHistory } from '@/types/workflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useDeleteWorkflow, useResetWorkflowVersionHistory, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow'
import { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow'
import Divider from '@/app/components/base/divider'
import Loading from './loading'
import Empty from './empty'
@@ -37,6 +37,10 @@ const VersionHistoryPanel = () => {
const currentVersion = useStore(s => s.currentVersion)
const setCurrentVersion = useStore(s => s.setCurrentVersion)
const userProfile = useAppContextSelector(s => s.userProfile)
const invalidAllLastRun = useInvalidAllLastRun(appDetail!.id)
const {
deleteAllInspectVars,
} = workflowStore.getState()
const { t } = useTranslation()

const {
@@ -125,6 +129,8 @@ const VersionHistoryPanel = () => {
type: 'success',
message: t('workflow.versionHistory.action.restoreSuccess'),
})
deleteAllInspectVars()
invalidAllLastRun()
},
onError: () => {
Toast.notify({
@@ -136,7 +142,7 @@ const VersionHistoryPanel = () => {
resetWorkflowVersionHistory()
},
})
}, [setShowWorkflowVersionHistoryPanel, handleSyncWorkflowDraft, workflowStore, handleRestoreFromPublishedWorkflow, resetWorkflowVersionHistory, t])
}, [setShowWorkflowVersionHistoryPanel, handleRestoreFromPublishedWorkflow, workflowStore, handleSyncWorkflowDraft, deleteAllInspectVars, invalidAllLastRun, t, resetWorkflowVersionHistory])

const { mutateAsync: deleteWorkflow } = useDeleteWorkflow(appDetail!.id)

@@ -149,6 +155,8 @@ const VersionHistoryPanel = () => {
message: t('workflow.versionHistory.action.deleteSuccess'),
})
resetWorkflowVersionHistory()
deleteAllInspectVars()
invalidAllLastRun()
},
onError: () => {
Toast.notify({
@@ -160,7 +168,7 @@ const VersionHistoryPanel = () => {
setDeleteConfirmOpen(false)
},
})
}, [t, deleteWorkflow, resetWorkflowVersionHistory])
}, [deleteWorkflow, t, resetWorkflowVersionHistory, deleteAllInspectVars, invalidAllLastRun])

const { mutateAsync: updateWorkflow } = useUpdateWorkflow(appDetail!.id)


+ 2
- 2
web/app/components/workflow/run/result-panel.tsx View File

@@ -17,11 +17,11 @@ import { LoopLogTrigger } from '@/app/components/workflow/run/loop-log'
import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log'

type ResultPanelProps = {
export type ResultPanelProps = {
nodeInfo?: NodeTracing
inputs?: string
process_data?: string
outputs?: string
outputs?: string | Record<string, any>
status: string
error?: string
elapsed_time?: number

+ 0
- 0
web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save