zxhlyh 1 месяц назад
Родитель
Сommit
e53052ab7a

+ 20 - 9
web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx

@@ -1,6 +1,6 @@
 'use client'
 import type { FC } from 'react'
-import React, { useCallback } from 'react'
+import React, { useCallback, useMemo } from 'react'
 import { useTranslation } from 'react-i18next'
 import produce from 'immer'
 import {
@@ -24,8 +24,9 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
 import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
 import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
 import cn from '@/utils/classnames'
+import type { FileEntity } from '@/app/components/base/file-uploader/types'
 
-interface Props {
+type Props = {
   payload: InputVar
   value: any
   onChange: (value: any) => void
@@ -94,6 +95,21 @@ const FormItem: FC<Props> = ({
   const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
   const isContext = type === InputVarType.contexts
   const isIterator = type === InputVarType.iterator
+  const singleFileValue = useMemo(() => {
+    if (payload.variable === '#files#')
+      return value?.[0] || []
+
+    return value ? [value] : []
+  }, [payload.variable, value])
+  const handleSingleFileChange = useCallback((files: FileEntity[]) => {
+    if (payload.variable === '#files#')
+      onChange(files)
+    else if (files.length)
+      onChange(files[0])
+    else
+      onChange(null)
+  }, [onChange, payload.variable])
+
   return (
     <div className={cn(className)}>
       {!isArrayLikeType && (
@@ -161,13 +177,8 @@ const FormItem: FC<Props> = ({
         }
         {(type === InputVarType.singleFile) && (
           <FileUploaderInAttachmentWrapper
-            value={value ? [value] : []}
-            onChange={(files) => {
-              if (files.length)
-                onChange(files[0])
-              else
-                onChange(null)
-            }}
+            value={singleFileValue}
+            onChange={handleSingleFileChange}
             fileConfig={{
               allowed_file_types: inStepRun
                 ? [

+ 4 - 1
web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx

@@ -50,8 +50,11 @@ function formatValue(value: string | any, type: InputVarType) {
   if (type === InputVarType.multiFiles)
     return getProcessedFiles(value)
 
-  if (type === InputVarType.singleFile)
+  if (type === InputVarType.singleFile) {
+    if (Array.isArray(value))
+      return getProcessedFiles(value)
     return getProcessedFiles([value])[0]
+  }
 
   return value
 }

+ 8 - 2
web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts

@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { unionBy } from 'lodash-es'
 import produce from 'immer'
@@ -139,6 +139,11 @@ 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)
+  const handleSetRunInputData = useCallback((data: Record<string, any>) => {
+    runInputDataRef.current = data
+    setRunInputData(data)
+  }, [])
   const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey].length : 0
   const [runResult, setRunResult] = useState<any>(null)
 
@@ -421,7 +426,8 @@ const useOneStepRun = <T>({
     handleRun,
     handleStop,
     runInputData,
-    setRunInputData,
+    runInputDataRef,
+    setRunInputData: handleSetRunInputData,
     runResult,
     iterationRunResult,
   }

+ 6 - 4
web/app/components/workflow/nodes/llm/panel.tsx

@@ -5,6 +5,7 @@ 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'
@@ -102,15 +103,16 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
       )
     }
 
-    if (isVisionModel) {
-      const variableName = data.vision.configs?.variable_selector?.[1] || t(`${i18nPrefix}.files`)!
+    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: variableName!,
+            label: currentVariable?.variable as any,
             variable: '#files#',
-            type: InputVarType.files,
+            type: currentVariable?.formType as any,
             required: false,
           }],
           values: { '#files#': visionFiles },

+ 8 - 7
web/app/components/workflow/nodes/llm/use-config.ts

@@ -306,6 +306,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
     handleRun,
     handleStop,
     runInputData,
+    runInputDataRef,
     setRunInputData,
     runResult,
     toVarInputs,
@@ -331,27 +332,27 @@ const useConfig = (id: string, payload: LLMNodeType) => {
   const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
     const newVars = {
       ...newPayload,
-      '#context#': runInputData['#context#'],
-      '#files#': runInputData['#files#'],
+      '#context#': runInputDataRef.current['#context#'],
+      '#files#': runInputDataRef.current['#files#'],
     }
     setRunInputData(newVars)
-  }, [runInputData, setRunInputData])
+  }, [runInputDataRef, setRunInputData])
 
   const contexts = runInputData['#context#']
   const setContexts = useCallback((newContexts: string[]) => {
     setRunInputData({
-      ...runInputData,
+      ...runInputDataRef.current,
       '#context#': newContexts,
     })
-  }, [runInputData, setRunInputData])
+  }, [runInputDataRef, setRunInputData])
 
   const visionFiles = runInputData['#files#']
   const setVisionFiles = useCallback((newFiles: any[]) => {
     setRunInputData({
-      ...runInputData,
+      ...runInputDataRef.current,
       '#files#': newFiles,
     })
-  }, [runInputData, setRunInputData])
+  }, [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]

+ 44 - 12
web/app/components/workflow/nodes/parameter-extractor/panel.tsx

@@ -6,6 +6,7 @@ import VarReferencePicker from '../_base/components/variable/var-reference-picke
 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'
@@ -21,6 +22,7 @@ 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'
@@ -51,6 +53,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
     handleReasoningModeChange,
     availableVars,
     availableNodesWithParent,
+    availableVisionVars,
     inputVarValues,
     varInputs,
     isVisionModel,
@@ -63,10 +66,50 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
     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='px-4 space-y-4'>
@@ -213,18 +256,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
         <BeforeRunForm
           nodeName={inputs.title}
           onHide={hideSingleRun}
-          forms={[
-            {
-              inputs: [{
-                label: t(`${i18nPrefix}.inputVar`)!,
-                variable: 'query',
-                type: InputVarType.paragraph,
-                required: true,
-              }, ...varInputs],
-              values: inputVarValues,
-              onChange: setInputVarValues,
-            },
-          ]}
+          forms={singleRunForms}
           runningStatus={runningStatus}
           onRun={handleRun}
           onStop={handleStop}

+ 25 - 1
web/app/components/workflow/nodes/parameter-extractor/use-config.ts

@@ -165,6 +165,10 @@ 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,
@@ -173,6 +177,13 @@ 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,13 +234,15 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
     handleRun,
     handleStop,
     runInputData,
+    runInputDataRef,
     setRunInputData,
     runResult,
   } = useOneStepRun<ParameterExtractorNodeType>({
     id,
     data: inputs,
     defaultRunInputData: {
-      query: '',
+      'query': '',
+      '#files#': [],
     },
   })
 
@@ -247,6 +260,14 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
     setRunInputData(newPayload)
   }, [setRunInputData])
 
+  const visionFiles = runInputData['#files#']
+  const setVisionFiles = useCallback((newFiles: any[]) => {
+    setRunInputData({
+      ...runInputDataRef.current,
+      '#files#': newFiles,
+    })
+  }, [runInputDataRef, setRunInputData])
+
   return {
     readOnly,
     handleInputVarChange,
@@ -264,6 +285,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
     hasSetBlockStatus,
     availableVars,
     availableNodesWithParent,
+    availableVisionVars,
     isSupportFunctionCall,
     handleReasoningModeChange,
     handleMemoryChange,
@@ -279,6 +301,8 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
     handleStop,
     runResult,
     setInputVarValues,
+    visionFiles,
+    setVisionFiles,
   }
 }
 

+ 44 - 12
web/app/components/workflow/nodes/question-classifier/panel.tsx

@@ -3,6 +3,7 @@ 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'
@@ -15,6 +16,7 @@ import ResultPanel from '@/app/components/workflow/run/result-panel'
 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'
 
@@ -36,6 +38,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
     hasSetBlockStatus,
     availableVars,
     availableNodesWithParent,
+    availableVisionVars,
     handleInstructionChange,
     inputVarValues,
     varInputs,
@@ -51,10 +54,50 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
     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 as any)['#files#']),
+        },
+      )
+    }
+
+    return forms
+  })()
+
   return (
     <div className='pt-2'>
       <div className='px-4 space-y-4'>
@@ -143,18 +186,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
         <BeforeRunForm
           nodeName={inputs.title}
           onHide={hideSingleRun}
-          forms={[
-            {
-              inputs: [{
-                label: t(`${i18nPrefix}.inputVars`)!,
-                variable: 'query',
-                type: InputVarType.paragraph,
-                required: true,
-              }, ...varInputs],
-              values: inputVarValues,
-              onChange: setInputVarValues,
-            },
-          ]}
+          forms={singleRunForms}
           runningStatus={runningStatus}
           onRun={handleRun}
           onStop={handleStop}

+ 25 - 1
web/app/components/workflow/nodes/question-classifier/use-config.ts

@@ -124,6 +124,10 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
     return [VarType.number, VarType.string].includes(varPayload.type)
   }, [])
 
+  const filterVisionInputVar = useCallback((varPayload: Var) => {
+    return [VarType.file, VarType.arrayFile].includes(varPayload.type)
+  }, [])
+
   const {
     availableVars,
     availableNodesWithParent,
@@ -132,6 +136,13 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
     filterVar: filterInputVar,
   })
 
+  const {
+    availableVars: availableVisionVars,
+  } = useAvailableVarList(id, {
+    onlyLeafNodeVar: false,
+    filterVar: filterVisionInputVar,
+  })
+
   const hasSetBlockStatus = {
     history: false,
     query: isChatMode ? checkHasQueryBlock(inputs.instruction) : false,
@@ -161,13 +172,15 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
     handleRun,
     handleStop,
     runInputData,
+    runInputDataRef,
     setRunInputData,
     runResult,
   } = useOneStepRun<QuestionClassifierNodeType>({
     id,
     data: inputs,
     defaultRunInputData: {
-      query: '',
+      'query': '',
+      '#files#': [],
     },
   })
 
@@ -195,6 +208,14 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
     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
   }, [])
@@ -212,6 +233,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
     hasSetBlockStatus,
     availableVars,
     availableNodesWithParent,
+    availableVisionVars,
     handleInstructionChange,
     varInputs,
     inputVarValues,
@@ -228,6 +250,8 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
     query,
     setQuery,
     runResult,
+    visionFiles,
+    setVisionFiles,
   }
 }
 

+ 30 - 0
web/app/components/workflow/nodes/utils.ts

@@ -0,0 +1,30 @@
+import type {
+  NodeOutPutVar,
+  ValueSelector,
+} from '@/app/components/workflow/types'
+import { InputVarType } from '@/app/components/workflow/types'
+
+export const findVariableWhenOnLLMVision = (valueSelector: ValueSelector, availableVars: NodeOutPutVar[]) => {
+  const currentVariableNode = availableVars.find((availableVar) => {
+    if (valueSelector[0] === 'sys' && availableVar.isStartNode)
+      return true
+
+    return valueSelector[0] === availableVar.nodeId
+  })
+  const currentVariable = currentVariableNode?.vars.find((variable) => {
+    if (valueSelector[0] === 'sys' && variable.variable === `sys.${valueSelector[1]}`)
+      return true
+    return variable.variable === valueSelector[1]
+  })
+
+  let formType = ''
+  if (currentVariable?.type === 'array[file]')
+    formType = InputVarType.multiFiles
+  if (currentVariable?.type === 'file')
+    formType = InputVarType.singleFile
+
+  return currentVariable && {
+    ...currentVariable,
+    formType,
+  }
+}