The frontend of feat: Persist Variables for Enhanced Debugging Workflow (#20699). Co-authored-by: jZonG <jzongcode@gmail.com>tags/1.5.0
| @@ -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) | |||
| }} | |||
| > | |||
| @@ -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 | |||
| @@ -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={` | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| } | |||
| }) | |||
| } | |||
| @@ -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> | |||
| ) | |||
| @@ -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( | |||
| @@ -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 | |||
| @@ -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 { | |||
| @@ -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) => { | |||
| @@ -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', | |||
| @@ -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 ( | |||
| <> | |||
| @@ -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 | |||
| @@ -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> | |||
| @@ -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 | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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>(( | |||
| { | |||
| @@ -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() | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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) | |||
| @@ -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 /> | |||
| @@ -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 | |||
| @@ -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> | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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, | |||
| @@ -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' /> | |||
| ) | |||
| } | |||
| @@ -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,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> | |||
| } | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -30,3 +30,5 @@ export type AssignerNodeType = CommonNodeType & { | |||
| version?: '1' | '2' | |||
| items: AssignerNodeOperation[] | |||
| } | |||
| export const writeModeTypesNum = [WriteMode.increment, WriteMode.decrement, WriteMode.multiply, WriteMode.divide] | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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} | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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,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, | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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,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, | |||
| @@ -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 | |||
| @@ -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 => ( | |||
| @@ -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 | |||
| @@ -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> | |||
| </> | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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 | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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> | |||
| ) | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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} | |||
| @@ -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} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -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 | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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,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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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) => { | |||
| @@ -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 | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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,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> | |||
| ) | |||
| } | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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) => { | |||
| @@ -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 | |||
| @@ -96,7 +96,7 @@ const AddBlock = ({ | |||
| onOpenChange={handleOpenChange} | |||
| disabled={nodesReadOnly} | |||
| onSelect={handleSelect} | |||
| placement='top-start' | |||
| placement='right-start' | |||
| offset={offset ?? { | |||
| mainAxis: 4, | |||
| crossAxis: -8, | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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)} | |||
| @@ -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 | |||
| @@ -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, | |||
| @@ -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) { | |||
| @@ -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> | |||
| ) | |||
| @@ -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> | |||
| ) | |||
| } | |||
| @@ -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) | |||
| @@ -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 | |||