revert: revert human input relevant code (#31766)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
QuantumGhost
2026-01-30 19:18:49 +08:00
committed by GitHub
parent ba568a634d
commit 90fe9abab7
470 changed files with 2082 additions and 32508 deletions

View File

@ -11,7 +11,6 @@ import {
End,
Home,
Http,
HumanInLoop,
IfElse,
Iteration,
KnowledgeBase,
@ -72,7 +71,6 @@ const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: strin
[BlockEnum.TriggerSchedule]: Schedule,
[BlockEnum.TriggerWebhook]: WebhookLine,
[BlockEnum.TriggerPlugin]: VariableX,
[BlockEnum.HumanInput]: HumanInLoop,
}
const getIcon = (type: BlockEnum, className: string) => {
@ -104,7 +102,6 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
[BlockEnum.HumanInput]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.KnowledgeBase]: 'bg-util-colors-warning-warning-500',
[BlockEnum.DataSource]: 'bg-components-icon-bg-midnight-solid',
[BlockEnum.TriggerSchedule]: 'bg-util-colors-violet-violet-500',

View File

@ -128,7 +128,6 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
BlockEnum.ListFilter,
BlockEnum.Agent,
BlockEnum.DataSource,
BlockEnum.HumanInput,
]
export const AGENT_OUTPUT_STRUCT: Var[] = [
@ -212,17 +211,6 @@ export const TOOL_OUTPUT_STRUCT: Var[] = [
},
]
export const HUMAN_INPUT_OUTPUT_STRUCT: Var[] = [
{
variable: '__action_id',
type: VarType.string,
},
{
variable: '__rendered_content',
type: VarType.string,
},
]
export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [
{
variable: '__is_success',

View File

@ -5,13 +5,12 @@ import codeDefault from '@/app/components/workflow/nodes/code/default'
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
import humanInputDefault from '@/app/components/workflow/nodes/human-input/default'
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
import iterationStartDefault from '@/app/components/workflow/nodes/iteration-start/default'
import iterationDefault from '@/app/components/workflow/nodes/iteration/default'
import knowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
import listOperatorDefault from '@/app/components/workflow/nodes/list-operator/default'
import llmDefault from '@/app/components/workflow/nodes/llm/default'
import loopEndDefault from '@/app/components/workflow/nodes/loop-end/default'
import loopStartDefault from '@/app/components/workflow/nodes/loop-start/default'
@ -42,5 +41,4 @@ export const WORKFLOW_COMMON_NODES = [
httpRequestDefault,
listOperatorDefault,
toolDefault,
humanInputDefault,
]

View File

@ -32,7 +32,7 @@ const RunMode = ({
handleWorkflowRunAllTriggersInWorkflow,
} = useWorkflowStartRun()
const { handleStopRun } = useWorkflowRun()
const { warningNodes } = useWorkflowRunValidation()
const { validateBeforeRun, warningNodes } = useWorkflowRunValidation()
const workflowRunningData = useStore(s => s.workflowRunningData)
const isListening = useStore(s => s.isListening)
@ -98,7 +98,14 @@ const RunMode = ({
// Placeholder for trigger-specific execution logic for schedule, webhook, plugin types
console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
}
}, [warningNodes, notify, t, handleWorkflowStartRunInWorkflow, handleWorkflowTriggerScheduleRunInWorkflow, handleWorkflowTriggerWebhookRunInWorkflow, handleWorkflowTriggerPluginRunInWorkflow, handleWorkflowRunAllTriggersInWorkflow])
}, [
validateBeforeRun,
handleWorkflowStartRunInWorkflow,
handleWorkflowTriggerScheduleRunInWorkflow,
handleWorkflowTriggerWebhookRunInWorkflow,
handleWorkflowTriggerPluginRunInWorkflow,
handleWorkflowRunAllTriggersInWorkflow,
])
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {

View File

@ -6,7 +6,7 @@ import { BlockEnum } from '../types'
import { useNodesMetaData } from './use-nodes-meta-data'
const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase || nodeType === BlockEnum.HumanInput))
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase))
return false
if (!inContainer && nodeType === BlockEnum.LoopEnd)

View File

@ -249,7 +249,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
})
return list
}, [nodes, edges, shouldCheckStartNode, nodesExtraData, buildInTools, customTools, workflowTools, language, dataSourceList, triggerPlugins, getToolIcon, strategyProviders, getCheckData, t, map])
}, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map, shouldCheckStartNode])
return needWarningNodes
}
@ -419,7 +419,7 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [store, workflowStore, getNodesAvailableVarList, shouldCheckStartNode, nodesExtraData, notify, t, updateDatasetsDetail, buildInTools, customTools, workflowTools, language, getCheckData, strategyProviders])
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
return {
handleCheckBeforePublish,

View File

@ -151,65 +151,11 @@ export const useEdgesInteractions = () => {
setEdges(newEdges)
}, [store, getNodesReadOnly])
const handleEdgeSourceHandleChange = useCallback((nodeId: string, oldHandleId: string, newHandleId: string) => {
if (getNodesReadOnly())
return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
// Find edges connected to the old handle
const affectedEdges = edges.filter(
edge => edge.source === nodeId && edge.sourceHandle === oldHandleId,
)
if (affectedEdges.length === 0)
return
// Update node metadata: remove old handle, add new handle
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
...affectedEdges.map(edge => ({ type: 'remove', edge })),
...affectedEdges.map(edge => ({
type: 'add',
edge: { ...edge, sourceHandle: newHandleId },
})),
],
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
setNodes(newNodes)
// Update edges to use new sourceHandle and regenerate edge IDs
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
if (edge.source === nodeId && edge.sourceHandle === oldHandleId) {
edge.sourceHandle = newHandleId
edge.id = `${edge.source}-${newHandleId}-${edge.target}-${edge.targetHandle}`
}
})
})
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.EdgeSourceHandleChange)
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
return {
handleEdgeEnter,
handleEdgeLeave,
handleEdgeDeleteByDeleteBranch,
handleEdgeDelete,
handleEdgesChange,
handleEdgeSourceHandleChange,
}
}

View File

@ -2,7 +2,7 @@ import type { Node, ToolWithProvider } from '@/app/components/workflow/types'
import type { SchemaTypeDefinition } from '@/service/use-common'
import type { FlowType } from '@/types/common'
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { useCallback, useMemo } from 'react'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
@ -14,7 +14,7 @@ import {
} from '@/service/use-tools'
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
import { fetchAllInspectVars } from '@/service/workflow'
import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type'
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
type Params = {
@ -37,18 +37,15 @@ export const useSetWorkflowVarsWithValue = ({
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const dataSourceList = useStore(s => s.dataSourceList)
const allPluginInfoList = {
buildInTools: buildInTools || [],
customTools: customTools || [],
workflowTools: workflowTools || [],
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
const allPluginInfoList = useMemo(() => {
return {
buildInTools: buildInTools || [],
customTools: customTools || [],
workflowTools: workflowTools || [],
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
}, [buildInTools, customTools, workflowTools, mcpTools, dataSourceList])
const setInspectVarsToStore = useCallback((inspectVars: VarInInspect[], passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]) => {
const setInspectVarsToStore = (inspectVars: VarInInspect[], passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]) => {
const { setNodesWithInspectVars } = workflowStore.getState()
const { getNodes } = store.getState()
@ -98,7 +95,7 @@ export const useSetWorkflowVarsWithValue = ({
return nodeWithVar
})
setNodesWithInspectVars(res)
}, [workflowStore, store, allPluginInfoList, schemaTypeDefinitions])
}
const fetchInspectVars = useCallback(async (params: {
passInVars?: boolean
@ -112,8 +109,7 @@ export const useSetWorkflowVarsWithValue = ({
const data = passInVars ? vars! : await fetchAllInspectVars(flowType, flowId)
setInspectVarsToStore(data, passedInAllPluginInfoList, passedInSchemaTypeDefinitions)
handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status
}, [invalidateConversationVarValues, invalidateSysVarValues, flowType, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus])
}, [invalidateConversationVarValues, invalidateSysVarValues, flowType, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus, schemaTypeDefinitions, getMatchedSchemaType])
return {
fetchInspectVars,
}

View File

@ -299,7 +299,6 @@ export const useNodesInteractions = () => {
|| connectingNode.data.type === BlockEnum.VariableAggregator)
&& node.data.type !== BlockEnum.IfElse
&& node.data.type !== BlockEnum.QuestionClassifier
&& node.data.type !== BlockEnum.HumanInput
) {
n.data._isEntering = true
}
@ -1018,7 +1017,6 @@ export const useNodesInteractions = () => {
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.HumanInput
) {
newNode.data._connectedSourceHandleIds = [sourceHandle]
}
@ -1055,7 +1053,6 @@ export const useNodesInteractions = () => {
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.HumanInput
&& nodeType !== BlockEnum.LoopEnd
) {
newEdge = {
@ -1247,7 +1244,6 @@ export const useNodesInteractions = () => {
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.HumanInput
&& nodeType !== BlockEnum.LoopEnd
) {
newNextEdge = {

View File

@ -27,7 +27,6 @@ export const WorkflowHistoryEvent = {
NodeDelete: 'NodeDelete',
EdgeDelete: 'EdgeDelete',
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
EdgeSourceHandleChange: 'EdgeSourceHandleChange',
NodeAdd: 'NodeAdd',
NodeResize: 'NodeResize',
NoteAdd: 'NoteAdd',

View File

@ -2,9 +2,6 @@ export * from './use-workflow-agent-log'
export * from './use-workflow-failed'
export * from './use-workflow-finished'
export * from './use-workflow-node-finished'
export * from './use-workflow-node-human-input-form-filled'
export * from './use-workflow-node-human-input-form-timeout'
export * from './use-workflow-node-human-input-required'
export * from './use-workflow-node-iteration-finished'
export * from './use-workflow-node-iteration-next'
export * from './use-workflow-node-iteration-started'
@ -13,7 +10,6 @@ export * from './use-workflow-node-loop-next'
export * from './use-workflow-node-loop-started'
export * from './use-workflow-node-retry'
export * from './use-workflow-node-started'
export * from './use-workflow-paused'
export * from './use-workflow-started'
export * from './use-workflow-text-chunk'
export * from './use-workflow-text-replace'

View File

@ -49,8 +49,6 @@ export const useWorkflowNodeFinished = () => {
if (data.node_type === BlockEnum.QuestionClassifier)
currentNode.data._runningBranchId = data?.outputs?.class_id
if (data.node_type === BlockEnum.HumanInput)
currentNode.data._runningBranchId = data?.outputs?.__action_id
}
})
setNodes(newNodes)

View File

@ -1,34 +0,0 @@
import type { HumanInputFormFilledResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeHumanInputFormFilled = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowNodeHumanInputFormFilled = useCallback((params: HumanInputFormFilledResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
draft.humanInputFormDataList.splice(currentFormIndex, 1)
}
if (!draft.humanInputFilledFormDataList) {
draft.humanInputFilledFormDataList = [data]
}
else {
draft.humanInputFilledFormDataList.push(data)
}
})
setWorkflowRunningData(newWorkflowRunningData)
}, [workflowStore])
return {
handleWorkflowNodeHumanInputFormFilled,
}
}

View File

@ -1,28 +0,0 @@
import type { HumanInputFormTimeoutResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeHumanInputFormTimeout = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowNodeHumanInputFormTimeout = useCallback((params: HumanInputFormTimeoutResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
draft.humanInputFormDataList[currentFormIndex].expiration_time = data.expiration_time
}
})
setWorkflowRunningData(newWorkflowRunningData)
}, [workflowStore])
return {
handleWorkflowNodeHumanInputFormTimeout,
}
}

View File

@ -1,60 +0,0 @@
import type { HumanInputRequiredResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import {
useStoreApi,
} from 'reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { NodeRunningStatus } from '@/app/components/workflow/types'
export const useWorkflowNodeHumanInputRequired = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
// Notice: Human input required !== Workflow Paused
const handleWorkflowNodeHumanInputRequired = useCallback((params: HumanInputRequiredResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (!draft.humanInputFormDataList) {
draft.humanInputFormDataList = [data]
}
else {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
if (currentFormIndex > -1) {
draft.humanInputFormDataList[currentFormIndex] = data
}
else {
draft.humanInputFormDataList.push(data)
}
}
const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id)
if (currentIndex > -1) {
draft.tracing![currentIndex] = {
...draft.tracing![currentIndex],
status: NodeRunningStatus.Paused,
}
}
})
setWorkflowRunningData(newWorkflowRunningData)
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Paused
})
setNodes(newNodes)
}, [store, workflowStore])
return {
handleWorkflowNodeHumanInputRequired,
}
}

View File

@ -33,23 +33,12 @@ export const useWorkflowNodeStarted = () => {
transform,
} = store.getState()
const nodes = getNodes()
const currentIndex = workflowRunningData?.tracing?.findIndex(item => item.node_id === data.node_id)
if (currentIndex && currentIndex > -1) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing![currentIndex] = {
...data,
status: NodeRunningStatus.Running,
}
}))
}
else {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}))
}
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}))
const {
setViewport,

View File

@ -1,26 +0,0 @@
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
export const useWorkflowPaused = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowPaused = useCallback(() => {
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Paused,
}
}))
}, [workflowStore])
return {
handleWorkflowPaused,
}
}

View File

@ -3,9 +3,6 @@ import {
useWorkflowFailed,
useWorkflowFinished,
useWorkflowNodeFinished,
useWorkflowNodeHumanInputFormFilled,
useWorkflowNodeHumanInputFormTimeout,
useWorkflowNodeHumanInputRequired,
useWorkflowNodeIterationFinished,
useWorkflowNodeIterationNext,
useWorkflowNodeIterationStarted,
@ -14,7 +11,6 @@ import {
useWorkflowNodeLoopStarted,
useWorkflowNodeRetry,
useWorkflowNodeStarted,
useWorkflowPaused,
useWorkflowStarted,
useWorkflowTextChunk,
useWorkflowTextReplace,
@ -36,10 +32,6 @@ export const useWorkflowRunEvent = () => {
const { handleWorkflowTextChunk } = useWorkflowTextChunk()
const { handleWorkflowTextReplace } = useWorkflowTextReplace()
const { handleWorkflowAgentLog } = useWorkflowAgentLog()
const { handleWorkflowPaused } = useWorkflowPaused()
const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired()
const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled()
const { handleWorkflowNodeHumanInputFormTimeout } = useWorkflowNodeHumanInputFormTimeout()
return {
handleWorkflowStarted,
@ -57,9 +49,5 @@ export const useWorkflowRunEvent = () => {
handleWorkflowTextChunk,
handleWorkflowTextReplace,
handleWorkflowAgentLog,
handleWorkflowPaused,
handleWorkflowNodeHumanInputFormFilled,
handleWorkflowNodeHumanInputRequired,
handleWorkflowNodeHumanInputFormTimeout,
}
}

View File

@ -22,15 +22,6 @@ export const useWorkflowStarted = () => {
edges,
setEdges,
} = store.getState()
if (workflowRunningData?.result?.status === WorkflowRunningStatus.Paused) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.result = {
...draft.result,
status: WorkflowRunningStatus.Running,
}
}))
return
}
setIterParallelLogMap(new Map())
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.task_id = task_id
@ -39,7 +30,6 @@ export const useWorkflowStarted = () => {
...data,
status: WorkflowRunningStatus.Running,
}
draft.resultText = ''
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {

View File

@ -116,7 +116,7 @@ export const useWorkflowVariables = () => {
schemaTypeDefinitions,
preferSchemaType,
})
}, [workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
}, [workflowStore, getVarType, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
return {
getNodeAvailableVars,

View File

@ -479,21 +479,11 @@ export const useNodesReadOnly = () => {
isRestoring,
} = workflowStore.getState()
return !!(
workflowRunningData?.result.status === WorkflowRunningStatus.Running
|| workflowRunningData?.result.status === WorkflowRunningStatus.Paused
|| historyWorkflowData
|| isRestoring
)
return !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring)
}, [workflowStore])
return {
nodesReadOnly: !!(
workflowRunningData?.result.status === WorkflowRunningStatus.Running
|| workflowRunningData?.result.status === WorkflowRunningStatus.Paused
|| historyWorkflowData
|| isRestoring
),
nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
getNodesReadOnly,
}
}

View File

@ -82,7 +82,7 @@ const FormItem: FC<Props> = ({
<div className="p-[1px]">
<VarBlockIcon type={nodeType || BlockEnum.Start} />
</div>
<div className="mx-0.5 max-w-[150px] truncate text-xs font-medium text-text-secondary" title={nodeName}>
<div className="mx-0.5 max-w-[150px] truncate text-xs font-medium text-gray-700" title={nodeName}>
{nodeName}
</div>
<Line3 className="mr-0.5"></Line3>

View File

@ -3,8 +3,7 @@ import type { FC } from 'react'
import type { Props as FormProps } from './form'
import type { Emoji } from '@/app/components/tools/types'
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
import type { NodeRunningStatus } from '@/app/components/workflow/types'
import type { HumanInputFormData } from '@/types/workflow'
import type { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
import * as React from 'react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@ -12,8 +11,7 @@ import Button from '@/app/components/base/button'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import Toast from '@/app/components/base/toast'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import SingleRunForm from '@/app/components/workflow/nodes/human-input/components/single-run-form'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { cn } from '@/utils/classnames'
import Form from './form'
@ -33,12 +31,6 @@ export type BeforeRunFormProps = {
showSpecialResultPanel?: boolean
existVarValuesInForms: Record<string, any>[]
filteredExistVarForms: FormProps[]
showGeneratedForm?: boolean
handleShowGeneratedForm?: (data: Record<string, any>) => void
handleHideGeneratedForm?: () => void
formData?: HumanInputFormData
handleSubmitHumanInputForm?: (data: any) => Promise<void>
handleAfterHumanInputStepRun?: () => void
} & Partial<SpecialResultPanelProps>
function formatValue(value: string | any, type: InputVarType) {
@ -70,24 +62,14 @@ function formatValue(value: string | any, type: InputVarType) {
}
const BeforeRunForm: FC<BeforeRunFormProps> = ({
nodeName,
nodeType,
onHide,
onRun,
forms,
filteredExistVarForms,
existVarValuesInForms,
showGeneratedForm = false,
handleShowGeneratedForm,
handleHideGeneratedForm,
formData,
handleSubmitHumanInputForm,
handleAfterHumanInputStepRun,
}) => {
const { t } = useTranslation()
const isHumanInput = nodeType === BlockEnum.HumanInput
const showBackButton = filteredExistVarForms.length > 0
const isFileLoaded = (() => {
if (!forms || forms.length === 0)
return true
@ -102,8 +84,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
return true
})()
const handleRunOrGenerateForm = () => {
const handleRun = () => {
let errMsg = ''
forms.forEach((form, i) => {
const existVarValuesInForm = existVarValuesInForms[i]
@ -154,30 +135,19 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
return
}
if (isHumanInput)
handleShowGeneratedForm?.(submitData)
else
onRun(submitData)
onRun(submitData)
}
const handleHumanInputFormSubmit = async (data: any) => {
await handleSubmitHumanInputForm?.(data)
handleAfterHumanInputStepRun?.()
}
const hasRun = useRef(false)
useEffect(() => {
// React 18 run twice in dev mode
if (hasRun.current)
return
hasRun.current = true
if (filteredExistVarForms.length === 0 && !isHumanInput)
if (filteredExistVarForms.length === 0)
onRun({})
if (filteredExistVarForms.length === 0 && isHumanInput)
handleShowGeneratedForm?.({})
}, [filteredExistVarForms, handleShowGeneratedForm, isHumanInput, onRun])
}, [filteredExistVarForms, onRun])
if (filteredExistVarForms.length === 0 && !isHumanInput)
if (filteredExistVarForms.length === 0)
return null
return (
@ -186,43 +156,23 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
onHide={onHide}
>
<div className="h-0 grow overflow-y-auto pb-4">
{!showGeneratedForm && (
<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>
)}
{showGeneratedForm && formData && (
<SingleRunForm
nodeName={nodeName}
showBackButton={showBackButton}
handleBack={handleHideGeneratedForm}
data={formData}
onSubmit={handleHumanInputFormSubmit}
/>
)}
{!showGeneratedForm && (
<div className="mt-4 flex justify-between space-x-2 px-4">
{!isHumanInput && (
<Button disabled={!isFileLoaded} variant="primary" className="w-0 grow space-x-2" onClick={handleRunOrGenerateForm}>
<div>{t(`${i18nPrefix}.startRun`, { ns: 'workflow' })}</div>
</Button>
)}
{isHumanInput && (
<Button disabled={!isFileLoaded} variant="primary" className="w-0 grow space-x-2" onClick={handleRunOrGenerateForm}>
<div>{t('nodes.humanInput.singleRun.button', { ns: 'workflow' })}</div>
</Button>
)}
</div>
)}
<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`, { ns: 'workflow' })}</div>
</Button>
</div>
</div>
</PanelWrap>
)

View File

@ -15,7 +15,6 @@ import type { QuestionClassifierNodeType } from '../../../question-classifier/ty
import type { TemplateTransformNodeType } from '../../../template-transform/types'
import type { ToolNodeType } from '../../../tool/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { CaseItem, Condition } from '@/app/components/workflow/nodes/if-else/types'
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
@ -42,7 +41,6 @@ import {
FILE_STRUCT,
getGlobalVars,
HTTP_REQUEST_OUTPUT_STRUCT,
HUMAN_INPUT_OUTPUT_STRUCT,
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
LLM_OUTPUT_STRUCT,
PARAMETER_EXTRACTOR_COMMON_STRUCT,
@ -52,7 +50,6 @@ import {
TOOL_OUTPUT_STRUCT,
} from '@/app/components/workflow/constants'
import DataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
import HumanInputNodeDefault from '@/app/components/workflow/nodes/human-input/default'
import ToolNodeDefault from '@/app/components/workflow/nodes/tool/default'
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
import {
@ -633,17 +630,6 @@ const formatItem = (
break
}
case BlockEnum.HumanInput: {
const outputSchema = HumanInputNodeDefault.getOutputVars?.(
data as HumanInputNodeType,
allPluginInfoList,
[],
{ schemaTypeDefinitions },
) || []
res.vars = [...outputSchema, ...HUMAN_INPUT_OUTPUT_STRUCT]
break
}
case 'env': {
res.vars = data.envList.map((env: EnvironmentVariable) => {
return {
@ -1501,13 +1487,6 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
res = valueSelectors
break
}
case BlockEnum.HumanInput: {
const payload = data as HumanInputNodeType
const formContent = payload.form_content
res = matchNotSystemVars([formContent])
break
}
}
return res || []
}
@ -1606,11 +1585,6 @@ export const getNodeUsedVarPassToServerKey = (
res = 'query'
break
}
case BlockEnum.HumanInput: {
res = `#${valueSelector.join('.')}#`
break
}
}
return res
}
@ -1947,15 +1921,6 @@ export const updateNodeVars = (
payload.variable = newVarSelector
break
}
case BlockEnum.HumanInput: {
const payload = data as HumanInputNodeType
payload.form_content = replaceOldVarInText(
payload.form_content,
oldVarSelector,
newVarSelector,
)
break
}
}
})
return newNode

View File

@ -71,8 +71,6 @@ type Props = {
availableNodes?: Node[]
availableVars?: NodeOutPutVar[]
isAddBtnTrigger?: boolean
trigger?: React.ReactNode
isJustShowValue?: boolean
schema?: Partial<CredentialFormSchema>
valueTypePlaceHolder?: string
isInTable?: boolean
@ -105,8 +103,6 @@ const VarReferencePicker: FC<Props> = ({
isFilterFileVar,
availableNodes: passedInAvailableNodes,
availableVars: passedInAvailableVars,
trigger,
isJustShowValue,
isAddBtnTrigger,
schema,
valueTypePlaceHolder,
@ -427,207 +423,204 @@ const VarReferencePicker: FC<Props> = ({
onOpenChange={setOpen}
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
>
{!!trigger && <PortalToFollowElemTrigger onClick={() => setOpen(!open)}>{trigger}</PortalToFollowElemTrigger>}
{!trigger && (
<WrapElem
onClick={() => {
if (readonly)
return
if (!isConstant)
setOpen(!open)
else
setControlFocus(Date.now())
}}
className="group/picker-trigger-wrap relative !flex"
>
<>
{isAddBtnTrigger
? (
<div>
<AddButton onClick={noop}></AddButton>
</div>
)
: (
<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'group/wrap relative flex h-8 w-full items-center', !isSupportConstantValue && 'rounded-lg bg-components-input-bg-normal p-1', isInTable && 'border-none bg-transparent', readonly && 'bg-components-input-bg-disabled', isJustShowValue && 'h-6 bg-transparent p-0')}>
{isSupportConstantValue
? (
<div
onClick={(e) => {
e.stopPropagation()
setOpen(false)
setControlFocus(Date.now())
}}
className="mr-1 flex h-full items-center space-x-1"
>
<TypeSelector
noLeft
trigger={(
<div className="radius-md flex h-8 items-center bg-components-input-bg-normal px-2">
<div className="system-sm-regular mr-1 text-components-input-text-filled">{varKindTypes.find(item => item.value === varKindType)?.label}</div>
<RiArrowDownSLine className="h-4 w-4 text-text-quaternary" />
</div>
)}
popupClassName="top-8"
readonly={readonly}
value={varKindType}
options={varKindTypes}
onChange={handleVarKindTypeChange}
showChecked
/>
</div>
)
: (!hasValue && (
<div className="ml-1.5 mr-1">
<Variable02 className={`h-4 w-4 ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'}`} />
</div>
))}
{isConstant
? (
<ConstantField
value={value as string}
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
schema={schemaWithDynamicSelect as CredentialFormSchema}
<WrapElem
onClick={() => {
if (readonly)
return
if (!isConstant)
setOpen(!open)
else
setControlFocus(Date.now())
}}
className="group/picker-trigger-wrap relative !flex"
>
<>
{isAddBtnTrigger
? (
<div>
<AddButton onClick={noop}></AddButton>
</div>
)
: (
<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'group/wrap relative flex h-8 w-full items-center', !isSupportConstantValue && 'rounded-lg bg-components-input-bg-normal p-1', isInTable && 'border-none bg-transparent', readonly && 'bg-components-input-bg-disabled')}>
{isSupportConstantValue
? (
<div
onClick={(e) => {
e.stopPropagation()
setOpen(false)
setControlFocus(Date.now())
}}
className="mr-1 flex h-full items-center space-x-1"
>
<TypeSelector
noLeft
trigger={(
<div className="radius-md flex h-8 items-center bg-components-input-bg-normal px-2">
<div className="system-sm-regular mr-1 text-components-input-text-filled">{varKindTypes.find(item => item.value === varKindType)?.label}</div>
<RiArrowDownSLine className="h-4 w-4 text-text-quaternary" />
</div>
)}
popupClassName="top-8"
readonly={readonly}
isLoading={isLoading}
value={varKindType}
options={varKindTypes}
onChange={handleVarKindTypeChange}
showChecked
/>
)
: (
<VarPickerWrap
onClick={() => {
if (readonly)
return
if (!isConstant)
setOpen(!open)
else
setControlFocus(Date.now())
}}
className="h-full grow"
>
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center rounded-lg bg-components-panel-bg py-1 pl-1')}>
<Tooltip noDecoration={isShowAPart} popupContent={tooltipPopup}>
<div className={cn('h-full items-center rounded-[5px] px-1.5', hasValue ? 'inline-flex bg-components-badge-white-to-dark' : 'flex')}>
{hasValue
? (
<>
{isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar && (
<div
className="flex items-center"
onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
e.stopPropagation()
handleVariableJump(outputVarNode?.id)
}
}}
>
<div className="h-3 px-[1px]">
{outputVarNode?.type && (
<VarBlockIcon
className="!text-text-primary"
type={outputVarNode.type}
/>
)}
</div>
<div
className="mx-0.5 truncate text-xs font-medium text-text-secondary"
title={outputVarNode?.title}
style={{
maxWidth: maxNodeNameWidth,
}}
>
{outputVarNode?.title}
</div>
<Line3 className="mr-0.5"></Line3>
</div>
)}
{isShowAPart && (
<div className="flex items-center">
<RiMoreLine className="h-3 w-3 text-text-secondary" />
<Line3 className="mr-0.5 text-divider-deep"></Line3>
</div>
)}
<div className="flex items-center text-text-accent">
{isLoading && <RiLoader4Line className="h-3.5 w-3.5 animate-spin text-text-secondary" />}
<VariableIconWithColor
variables={value as ValueSelector}
variableCategory={variableCategory}
isExceptionVariable={isException}
/>
<div
className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning', isGlobal && 'text-util-colors-orange-orange-600')}
title={varName}
style={{
maxWidth: maxVarNameWidth,
}}
>
{varName}
</div>
</div>
</div>
)
: (!hasValue && (
<div className="ml-1.5 mr-1">
<Variable02 className={`h-4 w-4 ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'}`} />
</div>
))}
{isConstant
? (
<ConstantField
value={value as string}
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
schema={schemaWithDynamicSelect as CredentialFormSchema}
readonly={readonly}
isLoading={isLoading}
/>
)
: (
<VarPickerWrap
onClick={() => {
if (readonly)
return
if (!isConstant)
setOpen(!open)
else
setControlFocus(Date.now())
}}
className="h-full grow"
>
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center rounded-lg bg-components-panel-bg py-1 pl-1')}>
<Tooltip noDecoration={isShowAPart} popupContent={tooltipPopup}>
<div className={cn('h-full items-center rounded-[5px] px-1.5', hasValue ? 'inline-flex bg-components-badge-white-to-dark' : 'flex')}>
{hasValue
? (
<>
{isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar && (
<div
className="system-xs-regular ml-0.5 truncate text-center capitalize text-text-tertiary"
title={type}
style={{
maxWidth: maxTypeWidth,
className="flex items-center"
onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
e.stopPropagation()
handleVariableJump(outputVarNode?.id)
}
}}
>
{type}
</div>
{!isValidVar && <RiErrorWarningFill className="ml-0.5 h-3 w-3 text-text-destructive" />}
</>
)
: (
<div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>
{isLoading
? (
<div className="flex items-center">
<RiLoader4Line className="mr-1 h-3.5 w-3.5 animate-spin text-text-secondary" />
<span>{placeholder ?? t('common.setVarValuePlaceholder', { ns: 'workflow' })}</span>
</div>
)
: (
placeholder ?? t('common.setVarValuePlaceholder', { ns: 'workflow' })
<div className="h-3 px-[1px]">
{outputVarNode?.type && (
<VarBlockIcon
className="!text-text-primary"
type={outputVarNode.type}
/>
)}
</div>
<div
className="mx-0.5 truncate text-xs font-medium text-text-secondary"
title={outputVarNode?.title}
style={{
maxWidth: maxNodeNameWidth,
}}
>
{outputVarNode?.title}
</div>
<Line3 className="mr-0.5"></Line3>
</div>
)}
{isShowAPart && (
<div className="flex items-center">
<RiMoreLine className="h-3 w-3 text-text-secondary" />
<Line3 className="mr-0.5 text-divider-deep"></Line3>
</div>
)}
<div className="flex items-center text-text-accent">
{isLoading && <RiLoader4Line className="h-3.5 w-3.5 animate-spin text-text-secondary" />}
<VariableIconWithColor
variables={value as ValueSelector}
variableCategory={variableCategory}
isExceptionVariable={isException}
/>
<div
className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning', isGlobal && 'text-util-colors-orange-orange-600')}
title={varName}
style={{
maxWidth: maxVarNameWidth,
}}
>
{varName}
</div>
</div>
)}
</div>
</Tooltip>
</div>
<div
className="system-xs-regular ml-0.5 truncate text-center capitalize text-text-tertiary"
title={type}
style={{
maxWidth: maxTypeWidth,
}}
>
{type}
</div>
{!isValidVar && <RiErrorWarningFill className="ml-0.5 h-3 w-3 text-text-destructive" />}
</>
)
: (
<div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>
{isLoading
? (
<div className="flex items-center">
<RiLoader4Line className="mr-1 h-3.5 w-3.5 animate-spin text-text-secondary" />
<span>{placeholder ?? t('common.setVarValuePlaceholder', { ns: 'workflow' })}</span>
</div>
)
: (
placeholder ?? t('common.setVarValuePlaceholder', { ns: 'workflow' })
)}
</div>
)}
</div>
</Tooltip>
</div>
</VarPickerWrap>
)}
{(hasValue && !readonly && !isInTable && !isJustShowValue) && (
<div
className="group invisible absolute right-1 top-[50%] h-5 translate-y-[-50%] cursor-pointer rounded-md p-1 hover:bg-state-base-hover group-hover/wrap:visible"
onClick={handleClearVar}
>
<RiCloseLine className="h-3.5 w-3.5 text-text-tertiary group-hover:text-text-secondary" />
</div>
)}
{!hasValue && valueTypePlaceHolder && (
<Badge
className=" absolute right-1 top-[50%] translate-y-[-50%] capitalize"
text={valueTypePlaceHolder}
uppercase={false}
/>
)}
</div>
)}
{!readonly && isInTable && (
<RemoveButton
className="absolute right-1 top-0.5 hidden group-hover/picker-trigger-wrap:block"
onClick={() => onRemove?.()}
/>
)}
</VarPickerWrap>
)}
{(hasValue && !readonly && !isInTable) && (
<div
className="group invisible absolute right-1 top-[50%] h-5 translate-y-[-50%] cursor-pointer rounded-md p-1 hover:bg-state-base-hover group-hover/wrap:visible"
onClick={handleClearVar}
>
<RiCloseLine className="h-3.5 w-3.5 text-text-tertiary group-hover:text-text-secondary" />
</div>
)}
{!hasValue && valueTypePlaceHolder && (
<Badge
className=" absolute right-1 top-[50%] translate-y-[-50%] capitalize"
text={valueTypePlaceHolder}
uppercase={false}
/>
)}
</div>
)}
{!readonly && isInTable && (
<RemoveButton
className="absolute right-1 top-0.5 hidden group-hover/picker-trigger-wrap:block"
onClick={() => onRemove?.()}
/>
)}
{!hasValue && typePlaceHolder && (
<Badge
className="absolute right-2 top-1.5"
text={typePlaceHolder}
uppercase={false}
/>
)}
</>
</WrapElem>
)}
{!hasValue && typePlaceHolder && (
<Badge
className="absolute right-2 top-1.5"
text={typePlaceHolder}
uppercase={false}
/>
)}
</>
</WrapElem>
<PortalToFollowElemContent
style={{
zIndex: zIndex || 100,

View File

@ -149,7 +149,6 @@ const Item: FC<ItemProps> = ({
}, [isHovering])
const handleChosen = (e: React.MouseEvent) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
if (!isSupportFileVar && isFile)
return
@ -190,11 +189,7 @@ const Item: FC<ItemProps> = ({
className,
)}
onClick={handleChosen}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}}
onMouseDown={e => e.preventDefault()}
>
<div className="flex w-0 grow items-center">
{!isFlat && (

View File

@ -27,7 +27,7 @@ const VariableLabel = ({
rightSlot,
}: VariablePayload) => {
const varColorClassName = useVarColor(variables, isExceptionVariable)
const isShowNodeLabel = !(isENV(variables) || isConversationVar(variables) || isGlobalVar(variables) || isRagVariableVar(variables))
const isHideNodeLabel = !(isENV(variables) || isConversationVar(variables) || isGlobalVar(variables) || isRagVariableVar(variables))
return (
<div
className={cn(
@ -37,7 +37,7 @@ const VariableLabel = ({
onClick={onClick}
ref={ref}
>
{isShowNodeLabel && (
{ isHideNodeLabel && (
<VariableNodeLabel
nodeType={nodeType}
nodeTitle={nodeTitle}

View File

@ -324,7 +324,7 @@ const BasePanel: FC<BasePanelProps> = ({
const currentDataSource = useMemo(() => {
if (data.type === BlockEnum.DataSource && data.provider_type !== DataSourceClassification.localFile)
return dataSourceList?.find(item => item.plugin_id === data.plugin_id)
}, [data.type, data.provider_type, data.plugin_id, dataSourceList])
}, [dataSourceList, data.provider_id, data.type, data.provider_type])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
@ -445,7 +445,6 @@ const BasePanel: FC<BasePanelProps> = ({
{...passedLogParams}
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
handleAfterHumanInputStepRun={handleAfterCustomSingleRun}
/>
)}

View File

@ -15,7 +15,6 @@ import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/no
import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params'
import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params'
import useHttpRequestSingleRunFormParams from '@/app/components/workflow/nodes/http/use-single-run-form-params'
import useHumanInputSingleRunFormParams from '@/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params'
import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params'
import useIterationSingleRunFormParams from '@/app/components/workflow/nodes/iteration/use-single-run-form-params'
import useKnowledgeBaseSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-base/use-single-run-form-params'
@ -23,16 +22,15 @@ import useKnowledgeRetrievalSingleRunFormParams from '@/app/components/workflow/
import useLLMSingleRunFormParams from '@/app/components/workflow/nodes/llm/use-single-run-form-params'
import useLoopSingleRunFormParams from '@/app/components/workflow/nodes/loop/use-single-run-form-params'
import useParameterExtractorSingleRunFormParams from '@/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params'
import useQuestionClassifierSingleRunFormParams from '@/app/components/workflow/nodes/question-classifier/use-single-run-form-params'
import useStartSingleRunFormParams from '@/app/components/workflow/nodes/start/use-single-run-form-params'
import useTemplateTransformSingleRunFormParams from '@/app/components/workflow/nodes/template-transform/use-single-run-form-params'
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
import useToolSingleRunFormParams from '@/app/components/workflow/nodes/tool/use-single-run-form-params'
import useTriggerPluginGetDataForCheckMore from '@/app/components/workflow/nodes/trigger-plugin/use-check-params'
import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { isSupportCustomRunForm } from '@/app/components/workflow/utils'
@ -65,7 +63,6 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.IterationStart]: undefined,
[BlockEnum.LoopStart]: undefined,
[BlockEnum.LoopEnd]: undefined,
[BlockEnum.HumanInput]: useHumanInputSingleRunFormParams,
[BlockEnum.DataSource]: undefined,
[BlockEnum.DataSourceEmpty]: undefined,
[BlockEnum.TriggerWebhook]: undefined,
@ -103,7 +100,6 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
[BlockEnum.Assigner]: undefined,
[BlockEnum.LoopStart]: undefined,
[BlockEnum.LoopEnd]: undefined,
[BlockEnum.HumanInput]: undefined,
[BlockEnum.DataSource]: undefined,
[BlockEnum.DataSourceEmpty]: undefined,
[BlockEnum.KnowledgeBase]: undefined,
@ -133,7 +129,6 @@ const useLastRun = <T>({
const isLoopNode = blockType === BlockEnum.Loop
const isAggregatorNode = blockType === BlockEnum.VariableAggregator
const isCustomRunNode = isSupportCustomRunForm(blockType)
const isHumanInputNode = blockType === BlockEnum.HumanInput
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const {
getData: getDataForCheckMore,
@ -343,7 +338,7 @@ const useLastRun = <T>({
return
if (blockType === BlockEnum.TriggerWebhook || blockType === BlockEnum.TriggerPlugin || blockType === BlockEnum.TriggerSchedule)
setShowVariableInspectPanel(true)
if (isCustomRunNode || isHumanInputNode) {
if (isCustomRunNode) {
showSingleRun()
return
}

View File

@ -24,7 +24,6 @@ import Assigner from '@/app/components/workflow/nodes/assigner/default'
import CodeDefault from '@/app/components/workflow/nodes/code/default'
import DocumentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
import HumanInputDefault from '@/app/components/workflow/nodes/human-input/default'
import IfElseDefault from '@/app/components/workflow/nodes/if-else/default'
import IterationDefault from '@/app/components/workflow/nodes/iteration/default'
import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
@ -70,7 +69,6 @@ const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
const { checkValid: checkIterationValid } = IterationDefault
const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault
const { checkValid: checkLoopValid } = LoopDefault
const { checkValid: checkHumanInputValid } = HumanInputDefault
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Partial<Record<BlockEnum, Function>> = {
@ -88,7 +86,6 @@ const checkValidFns: Partial<Record<BlockEnum, Function>> = {
[BlockEnum.Iteration]: checkIterationValid,
[BlockEnum.DocExtractor]: checkDocumentExtractorValid,
[BlockEnum.Loop]: checkLoopValid,
[BlockEnum.HumanInput]: checkHumanInputValid,
}
type RequestError = {
@ -316,7 +313,20 @@ const useOneStepRun = <T>({
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, flowType, flowId, id, store, appendNodeInspectVars, updateNodeInspectRunningState, invalidLastRun, isStartNode, isTriggerNode, invalidateSysVarValues, invalidateConversationVarValues])
}, [
isRunAfterSingleRun,
runningStatus,
flowId,
id,
store,
appendNodeInspectVars,
updateNodeInspectRunningState,
invalidLastRun,
isStartNode,
isTriggerNode,
invalidateSysVarValues,
invalidateConversationVarValues,
])
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const setNodeRunning = () => {

View File

@ -9,7 +9,6 @@ import {
RiCheckboxCircleFill,
RiErrorWarningFill,
RiLoader2Line,
RiPauseCircleFill,
} from '@remixicon/react'
import {
cloneElement,
@ -108,7 +107,7 @@ const BaseNode: FC<BaseNodeProps> = ({
showExceptionBorder,
} = useMemo(() => {
return {
showRunningBorder: (data._runningStatus === NodeRunningStatus.Running || data._runningStatus === NodeRunningStatus.Paused) && !showSelectedBorder,
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
showSuccessBorder: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && !showSelectedBorder,
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
@ -222,7 +221,7 @@ const BaseNode: FC<BaseNodeProps> = ({
)
}
{
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && (
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._isCandidate && (
<NodeSourceHandle
id={id}
data={data}
@ -288,27 +287,15 @@ const BaseNode: FC<BaseNodeProps> = ({
!!(data.type === BlockEnum.Loop && data._loopIndex) && LoopIndex
}
{
isLoading && <RiLoader2Line className="h-3.5 w-3.5 animate-spin text-text-accent" />
}
{
!isLoading && data._runningStatus === NodeRunningStatus.Failed && (
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
)
}
{
!isLoading && data._runningStatus === NodeRunningStatus.Exception && (
<RiAlertFill className="h-3.5 w-3.5 text-text-warning-secondary" />
)
}
{
!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && (
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
)
}
{
!isLoading && data._runningStatus === NodeRunningStatus.Paused && (
<RiPauseCircleFill className="h-3.5 w-3.5 text-text-warning-secondary" />
)
isLoading
? <RiLoader2Line className="h-3.5 w-3.5 animate-spin text-text-accent" />
: data._runningStatus === NodeRunningStatus.Failed
? <RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
: data._runningStatus === NodeRunningStatus.Exception
? <RiAlertFill className="h-3.5 w-3.5 text-text-warning-secondary" />
: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)
? <RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
: null
}
</div>
{

View File

@ -16,8 +16,6 @@ import EndNode from './end/node'
import EndPanel from './end/panel'
import HttpNode from './http/node'
import HttpPanel from './http/panel'
import HumanInputNode from './human-input/node'
import HumanInputPanel from './human-input/panel'
import IfElseNode from './if-else/node'
import IfElsePanel from './if-else/panel'
import IterationNode from './iteration/node'
@ -74,7 +72,6 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Agent]: AgentNode,
[BlockEnum.DataSource]: DataSourceNode,
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
[BlockEnum.HumanInput]: HumanInputNode,
[BlockEnum.TriggerSchedule]: TriggerScheduleNode,
[BlockEnum.TriggerWebhook]: TriggerWebhookNode,
[BlockEnum.TriggerPlugin]: TriggerPluginNode,
@ -103,7 +100,6 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Agent]: AgentPanel,
[BlockEnum.DataSource]: DataSourcePanel,
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
[BlockEnum.HumanInput]: HumanInputPanel,
[BlockEnum.TriggerSchedule]: TriggerSchedulePanel,
[BlockEnum.TriggerWebhook]: TriggerWebhookPanel,
[BlockEnum.TriggerPlugin]: TriggerPluginPanel,

View File

@ -1,27 +0,0 @@
'use client'
import type { FC } from 'react'
import type { FormInputItem } from '../types'
import * as React from 'react'
import InputField from '@/app/components/base/prompt-editor/plugins/hitl-input-block/input-field'
type Props = {
nodeId: string
onSave: (newPayload: FormInputItem) => void
onCancel: () => void
}
const AddInputField: FC<Props> = ({
nodeId,
onSave,
onCancel,
}) => {
return (
<InputField
nodeId={nodeId}
isEdit={false}
onChange={onSave}
onCancel={onCancel}
/>
)
}
export default React.memo(AddInputField)

View File

@ -1,111 +0,0 @@
import type { FC } from 'react'
import {
RiFontSize,
} from '@remixicon/react'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { cn } from '@/utils/classnames'
import { UserActionButtonType } from '../types'
const i18nPrefix = 'nodes.humanInput'
type Props = {
text: string
data: UserActionButtonType
onChange: (state: UserActionButtonType) => void
readonly?: boolean
}
const ButtonStyleDropdown: FC<Props> = ({
text = 'Button Text',
data,
onChange,
readonly,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const currentStyle = useMemo(() => {
switch (data) {
case UserActionButtonType.Primary:
return 'primary'
case UserActionButtonType.Default:
return 'secondary'
case UserActionButtonType.Accent:
return 'secondary-accent'
default:
return 'ghost'
}
}, [data])
return (
<PortalToFollowElem
open={open && !readonly}
onOpenChange={setOpen}
placement="bottom-end"
offset={{
mainAxis: 4,
crossAxis: 44,
}}
>
<PortalToFollowElemTrigger onClick={() => !readonly && setOpen(v => !v)}>
<div className={cn('flex items-center justify-center rounded-lg bg-components-button-tertiary-bg p-1', !readonly && 'cursor-pointer hover:bg-components-button-tertiary-bg-hover', open && 'bg-components-button-tertiary-bg-hover')}>
<Button size="small" className="pointer-events-none px-1" variant={currentStyle}>
<RiFontSize className="h-4 w-4" />
</Button>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 1000 }}>
<div className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-4 shadow-lg backdrop-blur-sm">
<div className="system-md-medium text-text-primary">{t(`${i18nPrefix}.userActions.chooseStyle`, { ns: 'workflow' })}</div>
<div className="mt-2 flex w-[324px] flex-wrap gap-1">
<div
className={cn(
'box-border flex h-[80px] w-[160px] cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-transparent bg-background-section hover:bg-background-section-burn',
data === UserActionButtonType.Primary && 'border-components-option-card-option-selected-border',
)}
onClick={() => onChange(UserActionButtonType.Primary)}
>
<Button variant="primary" className="pointer-events-none">{text}</Button>
</div>
<div
className={cn(
'box-border flex h-[80px] w-[160px] cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-transparent bg-background-section hover:bg-background-section-burn',
data === UserActionButtonType.Default && 'border-components-option-card-option-selected-border',
)}
onClick={() => onChange(UserActionButtonType.Default)}
>
<Button variant="secondary" className="pointer-events-none">{text}</Button>
</div>
<div
className={cn(
'box-border flex h-[80px] w-[160px] cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-transparent bg-background-section hover:bg-background-section-burn',
data === UserActionButtonType.Accent && 'border-components-option-card-option-selected-border',
)}
onClick={() => onChange(UserActionButtonType.Accent)}
>
<Button variant="secondary-accent" className="pointer-events-none">{text}</Button>
</div>
<div
className={cn(
'box-border flex h-[80px] w-[160px] cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-transparent bg-background-section hover:bg-background-section-burn',
data === UserActionButtonType.Ghost && 'border-components-option-card-option-selected-border',
)}
onClick={() => onChange(UserActionButtonType.Ghost)}
>
<Button variant="ghost" className="pointer-events-none">{text}</Button>
</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ButtonStyleDropdown

View File

@ -1,176 +0,0 @@
import type { EmailConfig } from '../../types'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { RiBugLine, RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/compat'
import { memo, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import MailBodyInput from './mail-body-input'
import Recipient from './recipient'
const i18nPrefix = 'nodes.humanInput'
type EmailConfigureModalProps = {
isShow: boolean
onClose: () => void
onConfirm: (data: EmailConfig) => void
config?: EmailConfig
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
}
const EmailConfigureModal = ({
isShow,
onClose,
onConfirm,
config,
nodesOutputVars = [],
availableNodes = [],
}: EmailConfigureModalProps) => {
const { t } = useTranslation()
const email = useAppContextWithSelector(s => s.userProfile.email)
const [recipients, setRecipients] = useState(config?.recipients || { whole_workspace: false, items: [] })
const [subject, setSubject] = useState(config?.subject || '')
const [body, setBody] = useState(config?.body || '{{#url#}}')
const [debugMode, setDebugMode] = useState(config?.debug_mode || false)
const checkValidConfig = useCallback(() => {
if (!subject.trim()) {
Toast.notify({
type: 'error',
message: 'subject is required',
})
return false
}
if (!body.trim()) {
Toast.notify({
type: 'error',
message: 'body is required',
})
return false
}
if (!/\{\{#url#\}\}/.test(body.trim())) {
Toast.notify({
type: 'error',
message: `body must contain one ${t('promptEditor.requestURL.item.title', { ns: 'common' })}`,
})
return false
}
if (!recipients || (recipients.items.length === 0 && !recipients.whole_workspace)) {
Toast.notify({
type: 'error',
message: 'recipients is required',
})
return false
}
return true
}, [recipients, subject, body, t])
const handleConfirm = useCallback(() => {
if (!checkValidConfig())
return
onConfirm({
recipients,
subject,
body,
debug_mode: debugMode,
})
}, [checkValidConfig, onConfirm, recipients, subject, body, debugMode])
return (
<Modal
isShow={isShow}
onClose={noop}
className="relative !max-w-[720px] !p-0"
>
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div>
<div className="space-y-1 p-6 pb-3">
<div className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.title`, { ns: 'workflow' })}</div>
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.description`, { ns: 'workflow' })}</div>
</div>
<div className="space-y-5 px-6 py-3">
<div>
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.subject`, { ns: 'workflow' })}
</div>
<Input
className="w-full"
value={subject}
onChange={e => setSubject(e.target.value)}
placeholder={t(`${i18nPrefix}.deliveryMethod.emailConfigure.subjectPlaceholder`, { ns: 'workflow' })}
/>
</div>
<div>
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.body`, { ns: 'workflow' })}
</div>
<MailBodyInput
value={body}
onChange={setBody}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
/>
</div>
<div>
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`, { ns: 'workflow' })}
</div>
<Recipient
data={recipients}
onChange={setRecipients}
/>
</div>
<Divider className="!my-0 !mt-5 !h-px" />
<div className="flex items-start justify-between gap-2 rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-3 pl-2.5 shadow-xs">
<div className="rounded-[4px] border border-divider-regular bg-components-icon-bg-orange-dark-solid p-0.5">
<RiBugLine className="h-3.5 w-3.5 text-text-primary-on-surface" />
</div>
<div className="grow space-y-1">
<div className="system-sm-medium text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.debugMode`, { ns: 'workflow' })}</div>
<div className="body-xs-regular text-text-tertiary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailConfigure.debugModeTip1`}
ns="workflow"
components={{ email: <span className="body-md-medium text-text-primary">{email}</span> }}
values={{ email }}
/>
<div>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.debugModeTip2`, { ns: 'workflow' })}</div>
</div>
</div>
<Switch
defaultValue={debugMode}
onChange={checked => setDebugMode(checked)}
/>
</div>
</div>
<div className="flex flex-row-reverse gap-2 p-6 pt-5">
<Button
variant="primary"
className="w-[72px]"
onClick={handleConfirm}
>
{t('operation.save', { ns: 'common' })}
</Button>
<Button
className="w-[72px]"
onClick={onClose}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
</Modal>
)
}
export default memo(EmailConfigureModal)

View File

@ -1,119 +0,0 @@
import type { DeliveryMethod, DeliveryMethodType, FormInputItem } from '../../types'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { produce } from 'immer'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
import MethodItem from './method-item'
import MethodSelector from './method-selector'
import UpgradeModal from './upgrade-modal'
const i18nPrefix = 'nodes.humanInput'
type Props = {
nodeId: string
value: DeliveryMethod[]
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
formContent?: string
formInputs?: FormInputItem[]
onChange: (value: DeliveryMethod[]) => void
readonly?: boolean
}
const DeliveryMethodForm: React.FC<Props> = ({
nodeId,
value,
nodesOutputVars,
availableNodes,
formContent,
formInputs,
onChange,
readonly,
}) => {
const { t } = useTranslation()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleMethodChange = (target: DeliveryMethod) => {
const newMethods = produce(value, (draft) => {
const index = draft.findIndex(method => method.type === target.type)
if (index !== -1)
draft[index] = target
})
onChange(newMethods)
handleSyncWorkflowDraft(true, true)
}
const handleMethodAdd = (newMethod: DeliveryMethod) => {
const newMethods = [...value, newMethod]
onChange(newMethods)
}
const handleMethodDelete = (type: DeliveryMethodType) => {
const newMethods = value.filter(method => method.type !== type)
onChange(newMethods)
}
const [showUpgradeModal, setShowUpgradeModal] = React.useState(false)
const handleShowUpgradeModal = () => {
setShowUpgradeModal(true)
}
const handleCloseUpgradeModal = () => {
setShowUpgradeModal(false)
}
return (
<div className="px-4 py-2">
<div className="mb-1 flex items-center justify-between">
<div className="flex items-center gap-0.5">
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.title`, { ns: 'workflow' })}</div>
<Tooltip
popupContent={t(`${i18nPrefix}.deliveryMethod.tooltip`, { ns: 'workflow' })}
/>
</div>
{!readonly && (
<div className="flex items-center px-1">
<MethodSelector
data={value}
onAdd={handleMethodAdd}
onShowUpgradeTip={handleShowUpgradeModal}
/>
</div>
)}
</div>
{!value.length && (
<div className="system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emptyTip`, { ns: 'workflow' })}</div>
)}
{value.length > 0 && (
<div className="space-y-1">
{value.map(method => (
<MethodItem
nodeId={nodeId}
method={method}
key={method.id}
onChange={handleMethodChange}
onDelete={handleMethodDelete}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
formContent={formContent}
formInputs={formInputs}
readonly={readonly}
/>
))}
</div>
)}
{showUpgradeModal && (
<UpgradeModal
isShow={showUpgradeModal}
onClose={handleCloseUpgradeModal}
/>
)}
</div>
)
}
export default DeliveryMethodForm

View File

@ -1,65 +0,0 @@
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import Placeholder from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder'
import { BlockEnum } from '@/app/components/workflow/types'
import { cn } from '@/utils/classnames'
type MailBodyInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
value?: string
onChange?: (text: string) => void
}
const MailBodyInput = ({
readOnly = false,
nodesOutputVars,
availableNodes = [],
value = '',
onChange,
}: MailBodyInputProps) => {
const { t } = useTranslation()
return (
<PromptEditor
wrapperClassName={cn(
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
className="caret:text-text-accent min-h-[128px]"
editable={!readOnly}
value={value}
requestURLBlock={{
show: true,
selectable: true,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('blocks.start', { ns: 'workflow' }),
type: BlockEnum.Start,
}
}
return acc
}, {} as Record<string, Pick<Node['data'], 'title' | 'type'>>),
}}
placeholder={<Placeholder hideBadge />}
onChange={onChange}
/>
)
}
export default MailBodyInput

View File

@ -1,212 +0,0 @@
import type { FC } from 'react'
import type { DeliveryMethod, EmailConfig, FormInputItem } from '../../types'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import {
RiDeleteBinLine,
RiEqualizer2Line,
RiMailSendFill,
RiRobot2Fill,
RiSendPlane2Line,
} from '@remixicon/react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge/index'
import Button from '@/app/components/base/button'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { cn } from '@/utils/classnames'
import { DeliveryMethodType } from '../../types'
import EmailConfigureModal from './email-configure-modal'
import TestEmailSender from './test-email-sender'
const i18nPrefix = 'nodes.humanInput'
type DeliveryMethodItemProps = {
nodeId: string
method: DeliveryMethod
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
formContent?: string
formInputs?: FormInputItem[]
onChange: (method: DeliveryMethod) => void
onDelete: (type: DeliveryMethodType) => void
readonly?: boolean
}
const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
nodeId,
method,
nodesOutputVars,
availableNodes,
formContent,
formInputs,
onChange,
onDelete,
readonly,
}) => {
const { t } = useTranslation()
const email = useAppContextWithSelector(s => s.userProfile.email)
const [isHovering, setIsHovering] = useState(false)
const [showEmailModal, setShowEmailModal] = useState(false)
const [showTestEmailModal, setShowTestEmailModal] = useState(false)
const handleEnableStatusChange = (enabled: boolean) => {
onChange({
...method,
enabled,
})
}
const handleConfigChange = (config: EmailConfig) => {
onChange({
...method,
config,
})
}
const emailSenderTooltipContent = useMemo(() => {
if (method.type !== DeliveryMethodType.Email) {
return ''
}
if (method.config?.debug_mode) {
return t(`${i18nPrefix}.deliveryMethod.emailSender.testSendTipInDebugMode`, { ns: 'workflow', email })
}
return t(`${i18nPrefix}.deliveryMethod.emailSender.testSendTip`, { ns: 'workflow' })
}, [method.type, method.config?.debug_mode, t, email])
const jumpToEmailConfigModal = useCallback(() => {
setShowTestEmailModal(false)
setShowEmailModal(true)
}, [])
return (
<>
<div
className={cn(
'group flex h-8 items-center justify-between rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-1.5 pr-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
isHovering && 'border-state-destructive-border bg-state-destructive-hover hover:bg-state-destructive-hover',
)}
>
<div className="flex items-center gap-1.5">
{method.type === DeliveryMethodType.WebApp && (
<div className="rounded-[4px] border border-divider-regular bg-components-icon-bg-indigo-solid p-0.5">
<RiRobot2Fill className="h-3.5 w-3.5 text-text-primary-on-surface" />
</div>
)}
{method.type === DeliveryMethodType.Email && (
<div className="rounded-[4px] border border-divider-regular bg-components-icon-bg-blue-solid p-0.5">
<RiMailSendFill className="h-3.5 w-3.5 text-text-primary-on-surface" />
</div>
)}
<div className="system-xs-medium capitalize text-text-secondary">{method.type}</div>
{method.type === DeliveryMethodType.Email
&& (method.config as EmailConfig)?.debug_mode
&& <Badge size="s" className="!px-1 !py-0.5">DEBUG</Badge>}
</div>
<div className="flex items-center gap-1">
{!readonly && (
<div className="hidden items-end gap-1 group-hover:flex">
{method.type === DeliveryMethodType.Email && method.config && (
<>
<Tooltip
popupContent={emailSenderTooltipContent}
asChild={false}
needsDelay={false}
>
<ActionButton
onClick={() => {
setShowTestEmailModal(true)
}}
>
<RiSendPlane2Line className="h-4 w-4" />
</ActionButton>
</Tooltip>
<Tooltip
popupContent={t('common.configure', { ns: 'workflow' })}
asChild={false}
needsDelay={false}
>
<ActionButton onClick={() => setShowEmailModal(true)}>
<RiEqualizer2Line className="h-4 w-4" />
</ActionButton>
</Tooltip>
</>
)}
<Tooltip
popupContent={t('operation.remove', { ns: 'common' })}
asChild={false}
needsDelay={false}
>
<div
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<ActionButton
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
onClick={() => onDelete(method.type)}
>
<RiDeleteBinLine className="h-4 w-4" />
</ActionButton>
</div>
</Tooltip>
</div>
)}
{(method.config || method.type === DeliveryMethodType.WebApp) && (
<Switch
defaultValue={method.enabled}
onChange={handleEnableStatusChange}
disabled={readonly}
/>
)}
{method.type === DeliveryMethodType.Email && !method.config && (
<Button
className="-mr-1"
size="small"
onClick={() => setShowEmailModal(true)}
disabled={readonly}
>
{t(`${i18nPrefix}.deliveryMethod.notConfigured`, { ns: 'workflow' })}
<Indicator color="orange" className="ml-1" />
</Button>
)}
</div>
</div>
{showEmailModal && (
<EmailConfigureModal
isShow={showEmailModal}
config={method.config as EmailConfig}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
onClose={() => setShowEmailModal(false)}
onConfirm={(data) => {
handleConfigChange(data)
setShowEmailModal(false)
}}
/>
)}
{showTestEmailModal && (
<TestEmailSender
nodeId={nodeId}
deliveryId={method.id}
isShow={showTestEmailModal}
config={method.config as EmailConfig}
formContent={formContent}
formInputs={formInputs}
nodesOutputVars={nodesOutputVars}
availableNodes={availableNodes}
onClose={() => setShowTestEmailModal(false)}
jumpToEmailConfigModal={jumpToEmailConfigModal}
/>
)}
</>
)
}
export default DeliveryMethodItem

View File

@ -1,222 +0,0 @@
'use client'
import type { FC } from 'react'
import type { DeliveryMethod } from '../../types'
import {
RiAddLine,
RiDiscordFill,
RiLightbulbFlashFill,
RiMailSendFill,
RiRobot2Fill,
} from '@remixicon/react'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { v4 as uuid4 } from 'uuid'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import { Slack, Teams } from '@/app/components/base/icons/src/public/other'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import useWorkflowNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { isTriggerWorkflow } from '@/app/components/workflow/utils/workflow-entry'
import { IS_CE_EDITION } from '@/config'
import { useProviderContextSelector } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
import { DeliveryMethodType } from '../../types'
const i18nPrefix = 'nodes.humanInput'
type MethodSelectorProps = {
data: DeliveryMethod[]
onAdd: (method: DeliveryMethod) => void
onShowUpgradeTip: () => void
}
const MethodSelector: FC<MethodSelectorProps> = ({
data,
onAdd,
onShowUpgradeTip,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const humanInputEmailDeliveryEnabled = useProviderContextSelector(s => s.humanInputEmailDeliveryEnabled)
const openRef = useRef(open)
const nodes = useWorkflowNodes()
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
const webAppDeliveryInfo = useMemo(() => {
const isTriggerMode = isTriggerWorkflow(nodes)
return {
disabled: isTriggerMode || data.some(method => method.type === DeliveryMethodType.WebApp),
added: data.some(method => method.type === DeliveryMethodType.WebApp),
isTriggerMode,
}
}, [data, nodes])
const emailDeliveryInfo = useMemo(() => {
return {
noPermission: !humanInputEmailDeliveryEnabled,
added: data.some(method => method.type === DeliveryMethodType.Email),
}
}, [data, humanInputEmailDeliveryEnabled])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement="bottom-end"
offset={{
mainAxis: 4,
crossAxis: 12,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton className={cn(open && 'bg-state-base-hover')}>
<RiAddLine className="h-4 w-4" />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-50">
<div className="w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
<div className="p-1">
<div
className={cn('relative flex cursor-pointer items-center gap-1 rounded-lg p-1 pl-3 hover:bg-state-base-hover', webAppDeliveryInfo.disabled && 'cursor-not-allowed bg-transparent hover:bg-transparent')}
onClick={() => {
if (webAppDeliveryInfo.disabled)
return
onAdd({
id: uuid4(),
type: DeliveryMethodType.WebApp,
enabled: true,
})
}}
>
<div className={cn('rounded-[4px] border border-divider-regular bg-components-icon-bg-indigo-solid p-1', webAppDeliveryInfo.disabled && 'opacity-50')}>
<RiRobot2Fill className="h-4 w-4 text-text-primary-on-surface" />
</div>
<div className={cn('p-1', webAppDeliveryInfo.disabled && 'opacity-50')}>
<div className="system-sm-medium mb-0.5 truncate text-text-primary">{t(`${i18nPrefix}.deliveryMethod.types.webapp.title`, { ns: 'workflow' })}</div>
<div className="system-xs-regular truncate text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.types.webapp.description`, { ns: 'workflow' })}</div>
</div>
{webAppDeliveryInfo.added && (
<div className="system-xs-regular absolute right-[12px] top-[13px] text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.added`, { ns: 'workflow' })}</div>
)}
{webAppDeliveryInfo.isTriggerMode && !webAppDeliveryInfo.added && (
<div className="system-xs-regular absolute right-[12px] top-[13px] text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.notAvailableInTriggerMode`, { ns: 'workflow' })}</div>
)}
</div>
<div
className={cn(
'relative flex cursor-pointer items-center gap-1 rounded-lg p-1 pl-3 hover:bg-state-base-hover',
emailDeliveryInfo.added && 'cursor-not-allowed bg-transparent hover:bg-transparent',
)}
onClick={() => {
if (emailDeliveryInfo.noPermission) {
onShowUpgradeTip()
return
}
if (emailDeliveryInfo.added)
return
onAdd({
id: uuid4(),
type: DeliveryMethodType.Email,
enabled: false,
})
}}
>
<div
className={cn(
'rounded-[4px] border border-divider-regular bg-components-icon-bg-blue-solid p-1',
emailDeliveryInfo.added && 'opacity-50',
)}
>
<RiMailSendFill className="h-4 w-4 text-text-primary-on-surface" />
</div>
<div className={cn('p-1', emailDeliveryInfo.added && 'opacity-50')}>
<div className="system-sm-medium mb-0.5 truncate text-text-primary">{t(`${i18nPrefix}.deliveryMethod.types.email.title`, { ns: 'workflow' })}</div>
<div className="system-xs-regular truncate text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.types.email.description`, { ns: 'workflow' })}</div>
</div>
{emailDeliveryInfo.added && (
<div className="system-xs-regular absolute right-[12px] top-[13px] text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.added`, { ns: 'workflow' })}</div>
)}
</div>
{/* Slack */}
<div
className={cn('relative flex cursor-pointer items-center gap-1 rounded-lg p-1 pl-3 hover:bg-state-base-hover', 'cursor-not-allowed bg-transparent hover:bg-transparent')}
>
<div className={cn('rounded-[4px] border border-divider-regular bg-background-default-dodge p-1', 'opacity-50')}>
<Slack className="h-4 w-4 text-text-primary-on-surface" />
</div>
<div className={cn('p-1', 'opacity-50')}>
<div className="system-sm-medium mb-0.5 truncate text-text-primary">{t(`${i18nPrefix}.deliveryMethod.types.slack.title`, { ns: 'workflow' })}</div>
<div className="system-xs-regular truncate text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.types.slack.description`, { ns: 'workflow' })}</div>
</div>
<div className="absolute right-[8px] top-[8px]">
<Badge className="h-4">COMING SOON</Badge>
</div>
</div>
{/* Teams */}
<div
className={cn('relative flex cursor-pointer items-center gap-1 rounded-lg p-1 pl-3 hover:bg-state-base-hover', 'cursor-not-allowed bg-transparent hover:bg-transparent')}
>
<div className={cn('rounded-[4px] border border-divider-regular bg-background-default-dodge p-1', 'opacity-50')}>
<Teams className="h-4 w-4 text-text-primary-on-surface" />
</div>
<div className={cn('p-1', 'opacity-50')}>
<div className="system-sm-medium mb-0.5 truncate text-text-primary">{t(`${i18nPrefix}.deliveryMethod.types.teams.title`, { ns: 'workflow' })}</div>
<div className="system-xs-regular truncate text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.types.teams.description`, { ns: 'workflow' })}</div>
</div>
<div className="absolute right-[8px] top-[8px]">
<Badge className="h-4">COMING SOON</Badge>
</div>
</div>
{/* Discord */}
<div
className={cn('relative flex cursor-pointer items-center gap-1 rounded-lg p-1 pl-3 hover:bg-state-base-hover', 'cursor-not-allowed bg-transparent hover:bg-transparent')}
>
<div className={cn('rounded-[4px] border border-divider-regular bg-components-icon-bg-indigo-solid p-0.5', 'opacity-50')}>
<RiDiscordFill className="h-5 w-5 text-text-primary-on-surface" />
</div>
<div className={cn('p-1', 'opacity-50')}>
<div className="system-sm-medium mb-0.5 truncate text-text-primary">{t(`${i18nPrefix}.deliveryMethod.types.discord.title`, { ns: 'workflow' })}</div>
<div className="system-xs-regular truncate text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.types.discord.description`, { ns: 'workflow' })}</div>
</div>
<div className="absolute right-[8px] top-[8px]">
<Badge className="h-4">COMING SOON</Badge>
</div>
</div>
</div>
</div>
{!IS_CE_EDITION && (
<div className="mt-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
<div className="flex items-center gap-2 px-4 py-3">
<div className={cn('rounded-[4px] border border-divider-regular bg-components-icon-bg-midnight-solid p-1')}>
<RiLightbulbFlashFill className="h-4 w-4 text-text-primary-on-surface" />
</div>
<div className="system-sm-regular text-text-secondary">
<div>{t(`${i18nPrefix}.deliveryMethod.contactTip1`, { ns: 'workflow' })}</div>
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.contactTip2`}
ns="workflow"
components={{ email: <a href="mailto:support@dify.ai" className="text-text-accent-light-mode-only">support@dify.ai</a> }}
/>
</div>
</div>
</div>
)}
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(MethodSelector)

View File

@ -1,183 +0,0 @@
import type { Recipient as RecipientItem } from '../../../types'
import type { Member } from '@/models/common'
import * as React from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { cn } from '@/utils/classnames'
import EmailItem from './email-item'
import MemberList from './member-list'
const i18nPrefix = 'nodes.humanInput'
type Props = {
email: string
value: RecipientItem[]
list: Member[]
onDelete: (recipient: RecipientItem) => void
onSelect: (value: string) => void
onAdd: (email: string) => void
disabled?: boolean
}
const EmailInput = ({
email,
value,
list,
onDelete,
onSelect,
onAdd,
disabled = false,
}: Props) => {
const { t } = useTranslation()
const inputRef = useRef<HTMLInputElement>(null)
const [isFocus, setIsFocus] = useState(false)
const [open, setOpen] = useState(false)
const [searchKey, setSearchKey] = useState('')
const selectedEmails = useMemo(() => {
return value.map((item) => {
const member = list.find(account => account.id === item.user_id)
return member ? { ...item, email: member.email, name: member.name } : item
})
}, [list, value])
const isErrorMember = useCallback((emailItem: RecipientItem) => emailItem.type === 'member' && list.every(item => item.id !== emailItem.user_id), [list])
const placeholder = useMemo(() => {
return (selectedEmails.length === 0 || isFocus)
? t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.placeholder`, { ns: 'workflow' })
: ''
}, [selectedEmails, t, isFocus])
const setInputFocus = () => {
if (disabled)
return
setIsFocus(true)
const input = inputRef.current?.children[0] as HTMLInputElement
input?.focus()
}
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchKey(e.target.value)
if (e.target.value.trim() === '') {
setOpen(false)
return
}
setOpen(true)
}
const handleSelect = (value: string) => {
setSearchKey('')
setOpen(false)
onSelect(value)
setInputFocus()
}
const checkEmailValid = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
return emailRegex.test(email)
}
const handleEmailAdd = () => {
const emailAddress = searchKey.trim()
if (!checkEmailValid(emailAddress))
return
if (value.some(item => item.email === emailAddress))
return
if (list.some(item => item.email === emailAddress)) {
const item = list.find(item => item.email === emailAddress)!
onSelect(item.id)
}
else {
onAdd(emailAddress)
}
setSearchKey('')
setOpen(false)
}
const handleInputBlur = () => {
setIsFocus(false)
handleEmailAdd()
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' || e.key === 'Tab' || e.key === ' ' || e.key === ',') {
e.preventDefault()
handleEmailAdd()
}
else if (e.key === 'Backspace') {
if (searchKey === '' && value.length > 0) {
e.preventDefault()
onDelete(value[value.length - 1])
setSearchKey('')
setOpen(false)
}
}
}
return (
<div className="p-1 pt-0">
<div
className={cn(
'flex max-h-24 min-h-16 flex-wrap overflow-y-auto rounded-lg border border-transparent bg-components-input-bg-normal p-2',
isFocus && 'border-components-input-border-active bg-components-input-bg-active shadow-xs',
!disabled && 'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
)}
onClick={setInputFocus}
>
{selectedEmails.map(item => (
<EmailItem
key={item.user_id || item.email}
email={email}
data={item as unknown as Member}
onDelete={onDelete}
disabled={disabled}
isError={isErrorMember(item)}
/>
))}
{!disabled && (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement="bottom-start"
offset={{
mainAxis: 4,
crossAxis: -40,
}}
>
<PortalToFollowElemTrigger className="block h-6 min-w-[166px]">
<input
ref={inputRef}
className="system-sm-regular h-6 min-w-[166px] appearance-none bg-transparent p-1 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder"
placeholder={placeholder}
onFocus={() => setIsFocus(true)}
onBlur={handleInputBlur}
value={searchKey}
onChange={handleValueChange}
onKeyDown={handleKeyDown}
/>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<MemberList
searchValue={searchKey}
list={list}
value={value}
onSearchChange={setSearchKey}
onSelect={handleSelect}
email={email}
hideSearch
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
)}
</div>
</div>
)
}
export default EmailInput

View File

@ -1,52 +0,0 @@
import type { Recipient as RecipientItem } from '../../../types'
import type { Member } from '@/models/common'
import { RiCloseCircleFill, RiErrorWarningFill } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import { cn } from '@/utils/classnames'
type Props = {
email: string
data: Member
disabled?: boolean
onDelete: (recipient: RecipientItem) => void
isError: boolean
}
const EmailItem = ({
email,
data,
onDelete,
disabled = false,
isError,
}: Props) => {
const { t } = useTranslation()
return (
<div
className={cn(
'flex h-6 items-center gap-1 rounded-full border border-components-panel-border-subtle bg-components-badge-white-to-dark p-1 shadow-xs',
isError && 'border-state-destructive-hover-alt bg-state-destructive-hover',
)}
onClick={e => e.stopPropagation()}
>
{isError && (
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
)}
{!isError && <Avatar avatar={data.avatar_url} size={16} name={data.name || data.email} />}
<div title={data.email} className="system-xs-regular max-w-[500px] truncate text-text-primary">
{email === data.email ? data.name : data.email}
{email === data.email && <span className="system-xs-regular text-text-tertiary">{t('members.you', { ns: 'common' })}</span>}
</div>
{!disabled && (
<RiCloseCircleFill
className="h-4 w-4 cursor-pointer text-text-quaternary hover:text-text-tertiary"
onClick={() => onDelete(data as unknown as RecipientItem)}
/>
)}
</div>
)
}
export default EmailItem

View File

@ -1,102 +0,0 @@
import type { RecipientData, Recipient as RecipientItem } from '../../../types'
import { RiGroupLine } from '@remixicon/react'
import { produce } from 'immer'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import { useAppContext } from '@/context/app-context'
import { useMembers } from '@/service/use-common'
import { cn } from '@/utils/classnames'
import EmailInput from './email-input'
import MemberSelector from './member-selector'
const i18nPrefix = 'nodes.humanInput'
type Props = {
data: RecipientData
onChange: (data: RecipientData) => void
}
const Recipient = ({
data,
onChange,
}: Props) => {
const { t } = useTranslation()
const { userProfile, currentWorkspace } = useAppContext()
const { data: members } = useMembers()
const accounts = members?.accounts || []
const handleMemberSelect = (id: string) => {
onChange(
produce(data, (draft) => {
draft.items.push({
type: 'member',
user_id: id,
})
}),
)
}
const handleEmailAdd = (email: string) => {
onChange(
produce(data, (draft) => {
draft.items.push({
type: 'external',
email,
})
}),
)
}
const handleDelete = (recipient: RecipientItem) => {
onChange(
produce(data, (draft) => {
if (recipient.type === 'member')
draft.items = draft.items.filter(item => item.user_id !== recipient.user_id)
else if (recipient.type === 'external')
draft.items = draft.items.filter(item => item.email !== recipient.email)
}),
)
}
return (
<div className="space-y-1">
<div className="rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs">
<div className="flex h-10 items-center justify-between pl-3 pr-1">
<div className="flex grow items-center gap-2">
<RiGroupLine className="h-4 w-4 text-text-secondary" />
<div className="system-sm-medium text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.title`, { ns: 'workflow' })}</div>
</div>
<div className="w-[86px]">
<MemberSelector
value={data.items}
email={userProfile.email}
list={accounts}
onSelect={handleMemberSelect}
/>
</div>
</div>
<EmailInput
email={userProfile.email}
value={data.items}
list={accounts}
onDelete={handleDelete}
onSelect={handleMemberSelect}
onAdd={handleEmailAdd}
/>
</div>
<div className="flex h-10 items-center gap-2 rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs">
<div className="flex h-5 w-5 items-center justify-center rounded-xl bg-components-icon-bg-blue-solid text-[14px]">
<span className="bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text font-semibold uppercase text-shadow-shadow-1 opacity-90">{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span>
</div>
<div className={cn('system-sm-medium grow text-text-secondary')}>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.allMembers`, { workspaceName: currentWorkspace.name.replace(/'/g, ''), ns: 'workflow' })}</div>
<Switch
defaultValue={data.whole_workspace}
onChange={checked => onChange({ ...data, whole_workspace: checked })}
/>
</div>
</div>
)
}
export default memo(Recipient)

View File

@ -1,91 +0,0 @@
'use client'
import type { FC } from 'react'
import type { Recipient } from '@/app/components/workflow/nodes/human-input/types'
import type { Member } from '@/models/common'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import Input from '@/app/components/base/input'
import { cn } from '@/utils/classnames'
const i18nPrefix = 'nodes.humanInput'
type Props = {
value: Recipient[]
searchValue: string
onSearchChange: (value: string) => void
list: Member[]
onSelect: (value: string) => void
email: string
hideSearch?: boolean
}
const MemberList: FC<Props> = ({ searchValue, list, value, onSearchChange, onSelect, email, hideSearch }) => {
const { t } = useTranslation()
const filteredList = useMemo(() => {
if (!list.length)
return []
if (!searchValue)
return list
return list.filter((account) => {
const name = account.name || ''
const email = account.email || ''
return name.toLowerCase().includes(searchValue.toLowerCase())
|| email.toLowerCase().includes(searchValue.toLowerCase())
})
}, [list, searchValue])
if (hideSearch && filteredList.length === 0)
return null
return (
<div className="min-w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
{!hideSearch && (
<div className="p-2 pb-1">
<Input
showLeftIcon
value={searchValue}
onChange={e => onSearchChange(e.target.value)}
/>
</div>
)}
{filteredList.length > 0 && (
<div className="max-h-[248px] overflow-y-auto p-1">
{filteredList.map(account => (
<div
key={account.id}
className={cn(
'group flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-2 pr-3 hover:bg-state-base-hover',
value.some(item => item.user_id === account.id) && 'bg-transparent hover:bg-transparent',
)}
onClick={() => {
if (value.some(item => item.user_id === account.id))
return
onSelect(account.id)
}}
>
<Avatar className={cn(value.some(item => item.user_id === account.id) && 'opacity-50')} avatar={account.avatar_url} size={24} name={account.name} />
<div className={cn('grow', value.some(item => item.user_id === account.id) && 'opacity-50')}>
<div className="system-sm-medium text-text-secondary">
{account.name}
{account.status === 'pending' && <span className="system-xs-medium ml-1 text-text-warning">{t('members.pending', { ns: 'common' })}</span>}
{email === account.email && <span className="system-xs-regular text-text-tertiary">{t('members.you', { ns: 'common' })}</span>}
</div>
<div className="system-xs-regular text-text-tertiary">{account.email}</div>
</div>
{!value.some(item => item.user_id === account.id) && (
<div className="system-xs-medium hidden text-text-accent group-hover:block">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.add`, { ns: 'workflow' })}</div>
)}
{value.some(item => item.user_id === account.id) && (
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.added`, { ns: 'workflow' })}</div>
)}
</div>
))}
</div>
)}
</div>
)
}
export default MemberList

View File

@ -1,69 +0,0 @@
'use client'
import type { FC } from 'react'
import type { Recipient } from '@/app/components/workflow/nodes/human-input/types'
import type { Member } from '@/models/common'
import {
RiContactsBookLine,
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { cn } from '@/utils/classnames'
import MemberList from './member-list'
const i18nPrefix = 'nodes.humanInput'
type Props = {
value: Recipient[]
email: string
onSelect: (value: string) => void
list: Member[]
}
const MemberSelector: FC<Props> = ({
value,
email,
onSelect,
list = [],
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState('')
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement="bottom-end"
offset={{
mainAxis: 4,
crossAxis: 35,
}}
>
<PortalToFollowElemTrigger
className="w-full"
onClick={() => setOpen(v => !v)}
>
<Button
className={cn('w-full justify-between', open && 'bg-state-accent-hover')}
variant="ghost-accent"
>
<RiContactsBookLine className="mr-1 h-4 w-4" />
<div className="">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.trigger`, { ns: 'workflow' })}</div>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<MemberList
searchValue={searchValue}
list={list}
value={value}
onSearchChange={setSearchValue}
onSelect={onSelect}
email={email}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default MemberSelector

View File

@ -1,372 +0,0 @@
import type { EmailConfig, FormInputItem } from '../../types'
import type {
Node,
NodeOutPutVar,
ValueSelector,
} from '@/app/components/workflow/types'
import { RiArrowRightSFill, RiCloseLine } from '@remixicon/react'
import { noop, unionBy } from 'es-toolkit/compat'
import { memo, useCallback, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Modal from '@/app/components/base/modal'
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
import FormItem from '@/app/components/workflow/nodes/_base/components/before-run-form/form-item'
import {
getNodeInfoById,
isConversationVar,
isENV,
isSystemVar,
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { InputVarType, VarType } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'
import { useMembers } from '@/service/use-common'
import { useTestEmailSender } from '@/service/use-workflow'
import { cn } from '@/utils/classnames'
import { isOutput } from '../../utils'
import EmailInput from './recipient/email-input'
const i18nPrefix = 'nodes.humanInput'
type EmailConfigureModalProps = {
nodeId: string
deliveryId: string
isShow: boolean
onClose: () => void
jumpToEmailConfigModal: () => void
config?: EmailConfig
formContent?: string
formInputs?: FormInputItem[]
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
}
const getOriginVar = (valueSelector: string[], list: NodeOutPutVar[]) => {
const targetVar = list.find(item => item.nodeId === valueSelector[0])
if (!targetVar)
return undefined
let curr: any = targetVar.vars
for (let i = 1; i < valueSelector.length; i++) {
const key = valueSelector[i]
const isLast = i === valueSelector.length - 1
if (Array.isArray(curr))
curr = curr.find((v: any) => v.variable.replace('conversation.', '') === key)
if (isLast)
return curr
else if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children
}
return undefined
}
const EmailSenderModal = ({
nodeId,
deliveryId,
isShow,
onClose,
jumpToEmailConfigModal,
config,
formContent,
formInputs,
nodesOutputVars = [],
availableNodes = [],
}: EmailConfigureModalProps) => {
const { t } = useTranslation()
const { userProfile, currentWorkspace } = useAppContext()
const appDetail = useAppStore(state => state.appDetail)
const { mutateAsync: testEmailSender } = useTestEmailSender()
const debugEnabled = !!config?.debug_mode
const onlyWholeTeam = config?.recipients?.whole_workspace && (!config?.recipients?.items || config?.recipients?.items.length === 0)
const onlySpecificUsers = !config?.recipients?.whole_workspace && config?.recipients?.items && config?.recipients?.items.length > 0
const combinedRecipients = config?.recipients?.whole_workspace && config?.recipients?.items && config?.recipients?.items.length > 0
const { data: members } = useMembers()
const accounts = members?.accounts || []
const generatedInputs = useMemo(() => {
const defaultValueSelectors = (formInputs || []).reduce((acc, input) => {
if (input.default.type === 'variable') {
acc.push(input.default.selector)
}
return acc
}, [] as ValueSelector[])
const valueSelectors = doGetInputVars((formContent || '') + (config?.body || ''))
const variables = unionBy([...valueSelectors, ...defaultValueSelectors], item => item.join('.')).map((item) => {
const varInfo = getNodeInfoById(availableNodes, item[0])?.data
return {
label: {
nodeType: varInfo?.type,
nodeName: varInfo?.title || availableNodes[0]?.data.title, // default start node title
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
isChatVar: isConversationVar(item),
},
variable: `#${item.join('.')}#`,
value_selector: item,
required: true,
}
})
const varInputs = variables.filter(item => !isENV(item.value_selector) && !isOutput(item.value_selector)).map((item) => {
const originalVar = getOriginVar(item.value_selector, nodesOutputVars)
if (!originalVar) {
return {
label: item.label || item.variable,
variable: item.variable,
type: InputVarType.textInput,
required: true,
value_selector: item.value_selector,
}
}
return {
label: item.label || item.variable,
variable: item.variable,
type: originalVar.type === VarType.number ? InputVarType.number : InputVarType.textInput,
required: true,
}
})
return varInputs
}, [availableNodes, config?.body, formContent, formInputs, nodesOutputVars])
const [inputs, setInputs] = useState<Record<string, unknown>>({})
const [collapsed, setCollapsed] = useState(!(generatedInputs.length > 0))
const [sendingEmail, setSendingEmail] = useState(false)
const [done, setDone] = useState(false)
const handleValueChange = (variable: string, v: string) => {
setInputs({
...inputs,
[variable]: v,
})
}
const confirmChecked = useMemo(() => {
for (const variable of generatedInputs) {
if (variable.required) {
const value = inputs[variable.variable]
if (value === undefined || value === null || value === '') {
return false
}
}
}
return true
}, [generatedInputs, inputs])
const handleConfirm = useCallback(async () => {
if (!confirmChecked)
return
setSendingEmail(true)
try {
await testEmailSender({
appID: appDetail?.id || '',
nodeID: nodeId,
deliveryID: deliveryId,
inputs,
})
setDone(true)
}
finally {
setSendingEmail(false)
}
}, [confirmChecked, testEmailSender, appDetail?.id, nodeId, deliveryId, inputs])
if (done) {
return (
<Modal
isShow={isShow}
onClose={noop}
className="relative !max-w-[480px] !p-0"
>
<div className="space-y-2 p-6 pb-3">
<div className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailSender.done`, { ns: 'workflow' })}</div>
{debugEnabled && (
<div className="system-md-regular text-text-secondary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugDone`}
ns="workflow"
components={{ email: <span className="system-md-semibold text-text-secondary"></span> }}
values={{ email: userProfile.email }}
/>
</div>
)}
{!debugEnabled && onlyWholeTeam && (
<div className="system-md-regular text-text-secondary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone2`}
ns="workflow"
components={{ team: <span className="system-md-medium text-text-secondary"></span> }}
values={{ team: currentWorkspace.name.replace(/'/g, '') }}
/>
</div>
)}
{!debugEnabled && onlySpecificUsers && (
<div className="system-md-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone3`, { ns: 'workflow' })}</div>
)}
{!debugEnabled && combinedRecipients && (
<div className="system-md-regular text-text-secondary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone1`}
ns="workflow"
components={{ team: <span className="system-md-medium text-text-secondary"></span> }}
values={{ team: currentWorkspace.name.replace(/'/g, '') }}
/>
</div>
)}
</div>
{(onlySpecificUsers || combinedRecipients) && !debugEnabled && (
<div className="px-5">
<EmailInput
disabled
email={userProfile.email}
value={config?.recipients?.items}
list={accounts}
onDelete={noop}
onSelect={noop}
onAdd={noop}
/>
</div>
)}
<div className="flex flex-row-reverse gap-2 p-6 pt-5">
<Button
variant="primary"
className="w-[72px]"
onClick={onClose}
>
{t('operation.ok', { ns: 'common' })}
</Button>
</div>
</Modal>
)
}
return (
<Modal
isShow={isShow}
onClose={noop}
className="relative !max-w-[480px] !p-0"
>
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div>
<div className="space-y-1 p-6 pb-3">
<div className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailSender.title`, { ns: 'workflow' })}</div>
{debugEnabled && (
<>
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip`, { ns: 'workflow' })}</div>
<div className="system-sm-regular text-text-secondary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`}
ns="workflow"
components={{ email: <span className="system-sm-semibold text-text-primary"></span> }}
values={{ email: userProfile.email }}
/>
</div>
</>
)}
{!debugEnabled && onlyWholeTeam && (
<div className="system-sm-regular text-text-secondary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip2`}
ns="workflow"
components={{ team: <span className="system-sm-semibold text-text-primary"></span> }}
values={{ team: currentWorkspace.name.replace(/'/g, '') }}
/>
</div>
)}
{!debugEnabled && onlySpecificUsers && (
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip3`, { ns: 'workflow' })}</div>
)}
{!debugEnabled && combinedRecipients && (
<div className="system-sm-regular text-text-secondary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip1`}
ns="workflow"
components={{ team: <span className="system-sm-semibold text-text-primary"></span> }}
values={{ team: currentWorkspace.name.replace(/'/g, '') }}
/>
</div>
)}
</div>
{(onlySpecificUsers || combinedRecipients) && !debugEnabled && (
<>
<div className="px-5">
<EmailInput
disabled
email={userProfile.email}
value={config?.recipients?.items}
list={accounts}
onDelete={noop}
onSelect={noop}
onAdd={noop}
/>
</div>
<div className="system-xs-regular px-6 pt-1 text-text-tertiary">
<Trans
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.tip`}
ns="workflow"
components={{
strong: <span onClick={jumpToEmailConfigModal} className="system-xs-regular cursor-pointer text-text-accent"></span>,
}}
/>
</div>
</>
)}
{/* vars */}
{generatedInputs.length > 0 && (
<>
<div className="px-6">
<Divider className="!mb-2 !mt-4 !h-px !w-12 bg-divider-regular" />
</div>
<div className="px-6 py-2">
<div className="group flex h-6 cursor-pointer items-center" onClick={() => setCollapsed(!collapsed)}>
<div className="system-sm-semibold-uppercase mr-1 text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.vars`, { ns: 'workflow' })}</div>
<RiArrowRightSFill className={cn('h-4 w-4 text-text-quaternary group-hover:text-text-primary', !collapsed && 'rotate-90')} />
</div>
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailSender.varsTip`, { ns: 'workflow' })}</div>
{!collapsed && (
<div className="mt-3 space-y-4">
{generatedInputs.map((variable, index) => (
<div
key={variable.variable}
className="mb-4 last-of-type:mb-0"
>
<FormItem
autoFocus={index === 0}
payload={variable}
value={inputs[variable.variable]}
onChange={v => handleValueChange(variable.variable, v)}
/>
</div>
))}
</div>
)}
</div>
</>
)}
<div className="flex flex-row-reverse gap-2 p-6 pt-5">
<Button
disabled={sendingEmail || !confirmChecked}
loading={sendingEmail}
variant="primary"
onClick={handleConfirm}
>
{t(`${i18nPrefix}.deliveryMethod.emailSender.send`, { ns: 'workflow' })}
</Button>
<Button
className="w-[72px]"
onClick={onClose}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
</div>
</Modal>
)
}
export default memo(EmailSenderModal)

View File

@ -1,76 +0,0 @@
import {
RiMailSendFill,
} from '@remixicon/react'
import { noop } from 'es-toolkit/compat'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import Modal from '@/app/components/base/modal'
import PremiumBadge from '@/app/components/base/premium-badge'
import { useModalContextSelector } from '@/context/modal-context'
import { cn } from '@/utils/classnames'
type UpgradeModalProps = {
isShow: boolean
onClose: () => void
}
const UpgradeModal: React.FC<UpgradeModalProps> = ({
isShow,
onClose,
}) => {
const { t } = useTranslation()
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
return (
<Modal
isShow={isShow}
onClose={noop}
className="relative !w-[580px] !max-w-[580px] !p-8"
>
<div className="pb-6">
<div
className={cn(
'mb-6 inline-flex rounded-xl border border-divider-regular bg-util-colors-blue-brand-blue-brand-500 p-2',
)}
>
<RiMailSendFill className="h-6 w-6 text-text-primary-on-surface" />
</div>
<p
className="title-3xl-semi-bold bg-[linear-gradient(271deg,_var(--components-input-border-active-prompt-1,_#155AEF)_-12.85%,_var(--components-input-border-active-prompt-2,_#0BA5EC)_95.4%)] bg-clip-text text-transparent"
>
{t('nodes.humanInput.deliveryMethod.upgradeTip', { ns: 'workflow' })}
</p>
<p className="system-md-regular mt-2 text-text-tertiary">
{t('nodes.humanInput.deliveryMethod.upgradeTipContent', { ns: 'workflow' })}
</p>
</div>
<div className="flex justify-end pt-5">
<Button
className="w-[72px]"
onClick={onClose}
>
{t('nodes.humanInput.deliveryMethod.upgradeTipHide', { ns: 'workflow' })}
</Button>
<PremiumBadge
size="custom"
color="blue"
allowHover={true}
className="ml-3 h-8 w-[93px]"
onClick={() => {
setShowPricingModal()
}}
>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="system-sm-medium">
<span className="p-1">
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
</span>
</div>
</PremiumBadge>
</div>
</Modal>
)
}
export default UpgradeModal

View File

@ -1,101 +0,0 @@
'use client'
import type { FC } from 'react'
import type { FormInputItem, UserAction } from '../types'
import type { ButtonProps } from '@/app/components/base/button'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
import { getButtonStyle } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
import { Markdown } from '@/app/components/base/markdown'
import { useStore } from '@/app/components/workflow/store'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { Note, rehypeNotes, rehypeVariable, Variable } from './variable-in-markdown'
const i18nPrefix = 'nodes.humanInput'
type FormContentPreviewProps = {
content: string
formInputs: FormInputItem[]
userActions: UserAction[]
onClose: () => void
}
const FormContentPreview: FC<FormContentPreviewProps> = ({
content,
formInputs,
userActions,
onClose,
}) => {
const { t } = useTranslation()
const panelWidth = useStore(state => state.panelWidth)
const nodes = useNodes()
const nodeName = React.useCallback((nodeId: string) => {
const node = nodes.find(n => n.id === nodeId)
return node?.data.title || nodeId
}, [nodes])
return (
<div
className="fixed top-[112px] z-10 max-h-[calc(100vh-116px)] w-[600px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg py-3 shadow-xl"
style={{
right: panelWidth + 8,
}}
>
<div className="flex h-[26px] items-center justify-between px-4">
<Badge uppercase className="border-text-accent-secondary text-text-accent-secondary">{t(`${i18nPrefix}.formContent.preview`, { ns: 'workflow' })}</Badge>
<ActionButton onClick={onClose}><RiCloseLine className="w-5 text-text-tertiary" /></ActionButton>
</div>
<div className="max-h-[calc(100vh-167px)] overflow-y-auto px-4">
<Markdown
content={content}
rehypePlugins={[rehypeVariable, rehypeNotes]}
customComponents={{
variable: ({ node }: { node: { properties?: { [key: string]: string } } }) => {
const path = node.properties?.['data-path'] as string
let newPath = path
if (path) {
newPath = path.replace(/#([^#.]+)([.#])/g, (match, nodeId, sep) => {
return `#${nodeName(nodeId)}${sep}`
})
}
return <Variable path={newPath} />
},
section: ({ node }: { node: { properties?: { [key: string]: string } } }) => (() => {
const name = node.properties?.['data-name'] as string
const input = formInputs.find(i => i.output_variable_name === name)
if (!input) {
return (
<div>
Can't find note:
{name}
</div>
)
}
const defaultInput = input.default
return (
<Note defaultInput={defaultInput!} nodeName={nodeName} />
)
})(),
}}
/>
<div className="mt-3 flex flex-wrap gap-1 py-1">
{userActions.map((action: UserAction) => (
<Button
key={action.id}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
>
{action.title}
</Button>
))}
</div>
<div className="system-xs-regular mt-1 text-text-tertiary">{t('nodes.humanInput.editor.previewTip', { ns: 'workflow' })}</div>
</div>
</div>
)
}
export default React.memo(FormContentPreview)

View File

@ -1,175 +0,0 @@
'use client'
import type { LexicalCommand } from 'lexical'
import type { FC } from 'react'
import type { FormInputItem } from '../types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import { INSERT_HITL_INPUT_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/hitl-input-block'
import { cn } from '@/utils/classnames'
import { useWorkflowVariableType } from '../../../hooks'
import { BlockEnum } from '../../../types'
import { isMac } from '../../../utils'
import AddInputField from './add-input-field'
type FormContentProps = {
nodeId: string
value: string
onChange: (value: string) => void
formInputs: FormInputItem[]
onFormInputsChange: (payload: FormInputItem[]) => void
onFormInputItemRename: (payload: FormInputItem, oldName: string) => void
onFormInputItemRemove: (varName: string) => void
editorKey: number
isExpand: boolean
availableVars: NodeOutPutVar[]
availableNodes: Node[]
readonly?: boolean
}
const Key: FC<{ children: React.ReactNode, className?: string }> = ({ children, className }) => {
return <span className={cn('system-kbd mx-0.5 inline-flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray text-text-placeholder ', className)}>{children}</span>
}
const CtrlKey: FC = () => {
return <Key className={cn('mr-0', !isMac() && 'w-7')}>{isMac() ? '⌘' : 'Ctrl'}</Key>
}
const FormContent: FC<FormContentProps> = ({
nodeId,
value,
onChange,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
editorKey,
isExpand,
availableVars,
availableNodes,
readonly,
}) => {
const { t } = useTranslation()
const getVarType = useWorkflowVariableType()
const [needToAddFormInput, setNeedToAddFormInput] = useState(false)
const [newFormInputs, setNewFormInputs] = useState<FormInputItem[]>([])
const handleInsertHITLNode = (onInsert: (command: LexicalCommand<unknown>, params: any) => void) => {
return (payload: FormInputItem) => {
const newFormInputs = [...(formInputs || []), payload]
onInsert(INSERT_HITL_INPUT_BLOCK_COMMAND, {
variableName: payload.output_variable_name,
nodeId,
formInputs: newFormInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
})
setNewFormInputs(newFormInputs)
setNeedToAddFormInput(true)
}
}
// avoid update formInputs would overwrite the value just inserted
useEffect(() => {
if (needToAddFormInput) {
onFormInputsChange(newFormInputs)
setNeedToAddFormInput(false)
}
}, [value])
const [isFocus, {
setTrue: setFocus,
setFalse: setBlur,
}] = useBoolean(false)
const workflowNodesMap = availableNodes.reduce((acc: any, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('blocks.start', { ns: 'workflow' }),
type: BlockEnum.Start,
}
}
return acc
}, {})
return (
<div
className={cn(
'flex grow flex-col rounded-[10px] border border-components-input-bg-normal bg-components-input-bg-normal pt-1',
isFocus && 'border-components-input-border-active bg-components-input-bg-active',
!isFocus && 'pb-[32px]',
readonly && 'pointer-events-none',
)}
>
<div className={cn('max-h-[300px] overflow-y-auto px-3', isExpand && 'h-0 max-h-full grow')}>
<PromptEditor
key={editorKey}
value={value}
onChange={onChange}
className={cn('min-h-[80px] ', isExpand && 'h-full')}
onFocus={setFocus}
onBlur={setBlur}
placeholder={t('nodes.humanInput.formContent.placeholder', { ns: 'workflow' })}
hitlInputBlock={{
show: true,
formInputs,
nodeId,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
variables: availableVars || [],
workflowNodesMap,
getVarType,
readonly,
}}
workflowVariableBlock={{
show: true,
variables: availableVars || [],
getVarType: getVarType as any,
workflowNodesMap,
}}
editable={!readonly}
shortcutPopups={readonly
? []
: [{
hotkey: ['mod', '/'],
Popup: ({ onClose, onInsert }) => (
<AddInputField
nodeId={nodeId}
onSave={handleInsertHITLNode(onInsert!)}
onCancel={onClose}
/>
),
}]}
/>
</div>
{isFocus && (
<div className="system-xs-regular flex h-8 shrink-0 items-center px-3 text-components-input-text-placeholder">
<Trans
i18nKey="nodes.humanInput.formContent.hotkeyTip"
ns="workflow"
components={
{
Key: <Key>/</Key>,
CtrlKey: <CtrlKey />,
}
}
/>
</div>
)}
</div>
)
}
export default React.memo(FormContent)

View File

@ -1,87 +0,0 @@
'use client'
import type { ButtonProps } from '@/app/components/base/button'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { HumanInputFormData } from '@/types/workflow'
import { RiArrowLeftLine } from '@remixicon/react'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item'
import { getButtonStyle, initializeInputs, splitByOutputVar } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
type Props = {
nodeName: string
data: HumanInputFormData
showBackButton?: boolean
handleBack?: () => void
onSubmit?: ({ inputs, action }: { inputs: Record<string, string>, action: string }) => Promise<void>
}
const FormContent = ({
nodeName,
data,
showBackButton,
handleBack,
onSubmit,
}: Props) => {
const { t } = useTranslation()
const defaultInputs = initializeInputs(data.inputs, data.resolved_default_values || {})
const contentList = splitByOutputVar(data.form_content)
const [inputs, setInputs] = useState(defaultInputs)
const [isSubmitting, setIsSubmitting] = useState(false)
const handleInputsChange = (name: string, value: string) => {
setInputs(prev => ({
...prev,
[name]: value,
}))
}
const submit = async (actionID: string) => {
setIsSubmitting(true)
await onSubmit?.({ inputs, action: actionID })
setIsSubmitting(false)
}
return (
<>
{showBackButton && (
<div className="flex items-center p-4 pb-1">
<div className="system-sm-semibold-uppercase flex cursor-pointer items-center text-text-accent" onClick={handleBack}>
<RiArrowLeftLine className="mr-1 h-4 w-4" />
{t('nodes.humanInput.singleRun.back', { ns: 'workflow' })}
</div>
<div className="system-xs-regular mx-1 text-divider-deep">/</div>
<div className="system-sm-semibold-uppercase text-text-secondary">{nodeName}</div>
</div>
)}
<div className="px-4 py-3">
{contentList.map((content, index) => (
<ContentItem
key={index}
content={content}
formInputFields={data.inputs}
inputs={inputs}
onInputChange={handleInputsChange}
/>
))}
<div className="flex flex-wrap gap-1 py-1">
{data.actions.map((action: UserAction) => (
<Button
key={action.id}
disabled={isSubmitting}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
onClick={() => submit(action.id)}
>
{action.title}
</Button>
))}
</div>
</div>
</>
)
}
export default React.memo(FormContent)

View File

@ -1,69 +0,0 @@
import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { cn } from '@/utils/classnames'
const i18nPrefix = 'nodes.humanInput'
type Props = {
timeout: number
unit: 'day' | 'hour'
onChange: (state: { timeout: number, unit: 'day' | 'hour' }) => void
readonly?: boolean
}
const TimeoutInput: FC<Props> = ({
timeout,
unit,
onChange,
readonly,
}) => {
const { t } = useTranslation()
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (/^\d*$/.test(value))
onChange({ timeout: Number(value) || 1, unit })
else
onChange({ timeout: 1, unit })
}
return (
<div className="flex items-center gap-1">
<Input
wrapperClassName="w-16"
type="number"
value={timeout}
min={1}
onChange={handleValueChange}
disabled={readonly}
/>
<div className="flex items-center gap-0.5 rounded-[10px] bg-components-segmented-control-bg-normal p-0.5">
<div
className={cn(
'rounded-lg px-2 py-1 text-text-tertiary',
!readonly && 'cursor-pointer hover:bg-state-base-hover hover:text-text-secondary',
unit === 'day' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm',
!readonly && unit === 'day' && 'hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => !readonly && onChange({ timeout, unit: 'day' })}
>
<div className="system-sm-medium p-0.5">{t(`${i18nPrefix}.timeout.days`, { ns: 'workflow' })}</div>
</div>
<div
className={cn(
'rounded-lg px-2 py-1 text-text-tertiary',
!readonly && 'cursor-pointer hover:bg-state-base-hover hover:text-text-secondary',
unit === 'hour' && 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-sm',
!readonly && unit === 'hour' && 'hover:bg-components-segmented-control-item-active-bg hover:text-text-accent-light-mode-only',
)}
onClick={() => !readonly && onChange({ timeout, unit: 'hour' })}
>
<div className="system-sm-medium p-0.5">{t(`${i18nPrefix}.timeout.hours`, { ns: 'workflow' })}</div>
</div>
</div>
</div>
)
}
export default TimeoutInput

View File

@ -1,111 +0,0 @@
import type { FC } from 'react'
import type { UserAction } from '../types'
import {
RiDeleteBinLine,
} from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import ButtonStyleDropdown from './button-style-dropdown'
const i18nPrefix = 'nodes.humanInput'
const ACTION_ID_MAX_LENGTH = 20
const BUTTON_TEXT_MAX_LENGTH = 20
type UserActionItemProps = {
data: UserAction
onChange: (state: UserAction) => void
onDelete: (id: string) => void
readonly?: boolean
}
const UserActionItem: FC<UserActionItemProps> = ({
data,
onChange,
onDelete,
readonly,
}) => {
const { t } = useTranslation()
const handleIDChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (!value.trim()) {
onChange({ ...data, id: '' })
return
}
// Convert spaces to underscores, then only allow characters matching /^[A-Za-z_][A-Za-z0-9_]*$/
const withUnderscores = value.replace(/ /g, '_')
let sanitized = withUnderscores
.split('')
.filter((char, index) => {
if (index === 0)
return /^[a-z_]$/i.test(char)
return /^\w$/.test(char)
})
.join('')
if (sanitized !== withUnderscores) {
Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.actionIdFormatTip`, { ns: 'workflow' }) })
return
}
// Limit to 20 characters
if (sanitized.length > ACTION_ID_MAX_LENGTH) {
sanitized = sanitized.slice(0, ACTION_ID_MAX_LENGTH)
Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.actionIdTooLong`, { ns: 'workflow', maxLength: ACTION_ID_MAX_LENGTH }) })
}
if (sanitized)
onChange({ ...data, id: sanitized })
}
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value
if (value.length > BUTTON_TEXT_MAX_LENGTH) {
value = value.slice(0, BUTTON_TEXT_MAX_LENGTH)
Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: BUTTON_TEXT_MAX_LENGTH }) })
}
onChange({ ...data, title: value })
}
return (
<div className="flex items-center gap-1">
<div className="shrink-0">
<Input
wrapperClassName="w-[120px]"
value={data.id}
placeholder={t(`${i18nPrefix}.userActions.actionNamePlaceholder`, { ns: 'workflow' })}
onChange={handleIDChange}
disabled={readonly}
/>
</div>
<div className="grow">
<Input
value={data.title}
placeholder={t(`${i18nPrefix}.userActions.buttonTextPlaceholder`, { ns: 'workflow' })}
onChange={handleTextChange}
disabled={readonly}
/>
</div>
<ButtonStyleDropdown
text={data.title}
data={data.button_style}
onChange={type => onChange({ ...data, button_style: type })}
readonly={readonly}
/>
{!readonly && (
<Button
className="px-2"
variant="tertiary"
onClick={() => onDelete(data.id)}
>
<RiDeleteBinLine className="h-4 w-4" />
</Button>
)}
</div>
)
}
export default UserActionItem

View File

@ -1,140 +0,0 @@
import type * as React from 'react'
import type { FormInputItemDefault } from '../types'
const variableRegex = /\{\{#(.+?)#\}\}/g
const noteRegex = /\{\{#\$(.+?)#\}\}/g
export function rehypeVariable() {
return (tree: any) => {
const iterate = (node: any, index: number, parent: any) => {
const value = node.value
variableRegex.lastIndex = 0
noteRegex.lastIndex = 0
if (node.type === 'text' && variableRegex.test(value) && !noteRegex.test(value)) {
let m: RegExpExecArray | null
let last = 0
const parts: any[] = []
variableRegex.lastIndex = 0
m = variableRegex.exec(value)
while (m !== null) {
if (m.index > last)
parts.push({ type: 'text', value: value.slice(last, m.index) })
parts.push({
type: 'element',
tagName: 'variable',
properties: { 'data-path': m[0].trim() },
children: [],
})
last = m.index + m[0].length
m = variableRegex.exec(value)
}
if (parts.length) {
if (last < value.length)
parts.push({ type: 'text', value: value.slice(last) })
parent.children.splice(index, 1, ...parts)
}
}
if (node.children) {
let i = 0
// Caution: can not use forEach. Because the length of tree.children may be changed because of change content: parent.children.splice(index, 1, ...parts)
while (i < node.children.length) {
iterate(node.children[i], i, node)
i++
}
}
}
let i = 0
// Caution: can not use forEach. Because the length of tree.children may be changed because of change content: parent.children.splice(index, 1, ...parts)
while (i < tree.children.length) {
iterate(tree.children[i], i, tree)
i++
}
}
}
export function rehypeNotes() {
return (tree: any) => {
const iterate = (node: any, index: number, parent: any) => {
const value = node.value
noteRegex.lastIndex = 0
if (node.type === 'text' && noteRegex.test(value)) {
let m: RegExpExecArray | null
let last = 0
const parts: any[] = []
noteRegex.lastIndex = 0
m = noteRegex.exec(value)
while (m !== null) {
if (m.index > last)
parts.push({ type: 'text', value: value.slice(last, m.index) })
const name = m[0].split('.').slice(-1)[0].replace('#}}', '')
parts.push({
type: 'element',
tagName: 'section',
properties: { 'data-name': name },
children: [],
})
last = m.index + m[0].length
m = noteRegex.exec(value)
}
if (parts.length) {
if (last < value.length)
parts.push({ type: 'text', value: value.slice(last) })
parent.children.splice(index, 1, ...parts)
parent.tagName = 'div' // h2 can not in p. In note content include the h2
}
}
if (node.children) {
let i = 0
// Caution: can not use forEach. Because the length of tree.children may be changed because of change content: parent.children.splice(index, 1, ...parts)
while (i < node.children.length) {
iterate(node.children[i], i, node)
i++
}
}
}
let i = 0
// Caution: can not use forEach. Because the length of tree.children may be changed because of change content: parent.children.splice(index, 1, ...parts)
while (i < tree.children.length) {
iterate(tree.children[i], i, tree)
i++
}
}
}
export const Variable: React.FC<{ path: string }> = ({ path }) => {
return (
<span className="text-text-accent">
{
path.replaceAll('.', '/')
.replace('{{#', '{{')
.replace('#}}', '}}')
}
</span>
)
}
export const Note: React.FC<{ defaultInput: FormInputItemDefault, nodeName: (nodeId: string) => string }> = ({ defaultInput, nodeName }) => {
const isVariable = defaultInput.type === 'variable'
const path = `{{#${defaultInput.selector.join('.')}#}}`
let newPath = path
if (path) {
newPath = path.replace(/#([^#.]+)([.#])/g, (match, nodeId, sep) => {
return `#${nodeName(nodeId)}${sep}`
})
}
return (
<div className="my-3 rounded-[10px] bg-components-input-bg-normal px-2.5 py-2">
{isVariable ? <Variable path={newPath} /> : <span>{defaultInput.value}</span>}
</div>
)
}

View File

@ -1,75 +0,0 @@
import type { NodeDefault, Var } from '../../types'
import type { HumanInputNodeType } from './types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
const i18nPrefix = 'nodes.humanInput.errorMsg'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Logic,
sort: 1,
type: BlockEnum.HumanInput,
})
const buildOutputVars = (variables: string[]): Var[] => {
return variables.map((variable) => {
return {
variable,
type: VarType.string,
}
})
}
const nodeDefault: NodeDefault<HumanInputNodeType> = {
metaData,
defaultValue: {
delivery_methods: [],
user_actions: [],
form_content: '',
inputs: [],
timeout: 3,
timeout_unit: 'day',
},
checkValid(payload: HumanInputNodeType, t: (str: string, options: Record<string, unknown>) => string) {
let errorMessages = ''
if (!errorMessages && !payload.delivery_methods.length)
errorMessages = t(`${i18nPrefix}.noDeliveryMethod`, { ns: 'workflow' })
if (!errorMessages && payload.delivery_methods.length > 0 && !payload.delivery_methods.some(method => method.enabled))
errorMessages = t(`${i18nPrefix}.noDeliveryMethodEnabled`, { ns: 'workflow' })
if (!errorMessages && !payload.user_actions.length)
errorMessages = t(`${i18nPrefix}.noUserActions`, { ns: 'workflow' })
if (!errorMessages && payload.user_actions.length > 0) {
const actionIds = payload.user_actions.map(action => action.id)
const hasDuplicateIds = actionIds.length !== new Set(actionIds).size
if (hasDuplicateIds)
errorMessages = t(`${i18nPrefix}.duplicateActionId`, { ns: 'workflow' })
}
if (!errorMessages && payload.user_actions.length > 0) {
const hasEmptyId = payload.user_actions.some(action => !action.id?.trim())
if (hasEmptyId)
errorMessages = t(`${i18nPrefix}.emptyActionId`, { ns: 'workflow' })
}
if (!errorMessages && payload.user_actions.length > 0) {
const hasEmptyTitle = payload.user_actions.some(action => !action.title?.trim())
if (hasEmptyTitle)
errorMessages = t(`${i18nPrefix}.emptyActionTitle`, { ns: 'workflow' })
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
getOutputVars(payload, _allPluginInfoList, _ragVars) {
const variables = payload.inputs.map(input => input.output_variable_name)
return buildOutputVars(variables)
},
}
export default nodeDefault

View File

@ -1,85 +0,0 @@
import type { DeliveryMethod, HumanInputNodeType, UserAction } from '../types'
import { produce } from 'immer'
import { useState } from 'react'
import { useUpdateNodeInternals } from 'reactflow'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useEdgesInteractions } from '@/app/components/workflow/hooks/use-edges-interactions'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useFormContent from './use-form-content'
const useConfig = (id: string, payload: HumanInputNodeType) => {
const updateNodeInternals = useUpdateNodeInternals()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<HumanInputNodeType>(id, payload)
const formContentHook = useFormContent(id, payload)
const { handleEdgeDeleteByDeleteBranch, handleEdgeSourceHandleChange } = useEdgesInteractions()
const [structuredOutputCollapsed, setStructuredOutputCollapsed] = useState(true)
const handleDeliveryMethodChange = (methods: DeliveryMethod[]) => {
setInputs({
...inputs,
delivery_methods: methods,
})
}
const handleUserActionAdd = (newAction: UserAction) => {
setInputs({
...inputs,
user_actions: [...inputs.user_actions, newAction],
})
}
const handleUserActionChange = (index: number, updatedAction: UserAction) => {
const newActions = produce(inputs.user_actions, (draft) => {
if (draft[index])
draft[index] = updatedAction
})
setInputs({
...inputs,
user_actions: newActions,
})
// Update edges to use the new handle
const oldAction = inputs.user_actions[index]
if (oldAction && oldAction.id !== updatedAction.id) {
handleEdgeSourceHandleChange(id, oldAction.id, updatedAction.id)
updateNodeInternals(id) // Update handles
}
}
const handleUserActionDelete = (actionId: string) => {
const newActions = inputs.user_actions.filter(action => action.id !== actionId)
setInputs({
...inputs,
user_actions: newActions,
})
// Delete edges connected to this action
handleEdgeDeleteByDeleteBranch(id, actionId)
}
const handleTimeoutChange = ({ timeout, unit }: { timeout: number, unit: 'hour' | 'day' }) => {
setInputs({
...inputs,
timeout,
timeout_unit: unit,
})
}
return {
readOnly,
inputs,
handleDeliveryMethodChange,
handleUserActionAdd,
handleUserActionChange,
handleUserActionDelete,
handleTimeoutChange,
structuredOutputCollapsed,
setStructuredOutputCollapsed,
...formContentHook,
}
}
export default useConfig

View File

@ -1,65 +0,0 @@
import type { FormInputItem, HumanInputNodeType } from '../types'
import { produce } from 'immer'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useWorkflow } from '@/app/components/workflow/hooks'
import useNodeCrud from '../../_base/hooks/use-node-crud'
const useFormContent = (id: string, payload: HumanInputNodeType) => {
const [editorKey, setEditorKey] = useState(0)
const { inputs, setInputs } = useNodeCrud<HumanInputNodeType>(id, payload)
const { handleOutVarRenameChange } = useWorkflow()
const inputsRef = useRef(inputs)
useEffect(() => {
inputsRef.current = inputs
}, [inputs])
const handleFormContentChange = useCallback((value: string) => {
setInputs({
...inputs,
form_content: value,
})
}, [inputs, setInputs])
const handleFormInputsChange = useCallback((formInputs: FormInputItem[]) => {
setInputs({
...inputs,
inputs: formInputs,
})
setEditorKey(editorKey => editorKey + 1)
}, [inputs, setInputs])
const handleFormInputItemRename = useCallback((payload: FormInputItem, oldName: string) => {
const inputs = inputsRef.current
const newInputs = produce(inputs, (draft) => {
draft.form_content = draft.form_content.replaceAll(`{{#$output.${oldName}#}}`, `{{#$output.${payload.output_variable_name}#}}`)
draft.inputs = draft.inputs.map(item => item.output_variable_name === oldName ? payload : item)
if (!draft.inputs.find(item => item.output_variable_name === payload.output_variable_name))
draft.inputs = [...draft.inputs, payload]
})
setInputs(newInputs)
setEditorKey(editorKey => editorKey + 1)
// Update downstream nodes that reference this variable
if (oldName !== payload.output_variable_name)
handleOutVarRenameChange(id, [id, oldName], [id, payload.output_variable_name])
}, [setInputs, handleOutVarRenameChange, id])
const handleFormInputItemRemove = useCallback((varName: string) => {
const inputs = inputsRef.current
const newInputs = produce(inputs, (draft) => {
draft.form_content = draft.form_content.replaceAll(`{{#$output.${varName}#}}`, '')
draft.inputs = draft.inputs.filter(item => item.output_variable_name !== varName)
})
setInputs(newInputs)
setEditorKey(editorKey => editorKey + 1)
}, [setInputs])
return {
editorKey,
handleFormContentChange,
handleFormInputsChange,
handleFormInputItemRename,
handleFormInputItemRemove,
}
}
export default useFormContent

View File

@ -1,128 +0,0 @@
import type { HumanInputNodeType } from '../types'
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
import type { InputVar } from '@/app/components/workflow/types'
import type { HumanInputFormData } from '@/types/workflow'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import { fetchHumanInputNodeStepRunForm, submitHumanInputNodeStepRunForm } from '@/service/workflow'
import { AppModeEnum } from '@/types/app'
import useNodeCrud from '../../_base/hooks/use-node-crud'
import { isOutput } from '../utils'
const i18nPrefix = 'nodes.humanInput'
type Params = {
id: string
payload: HumanInputNodeType
runInputData: Record<string, string>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, string>) => void
}
const useSingleRunFormParams = ({
id,
payload,
runInputData,
getInputVars,
setRunInputData,
}: Params) => {
const { t } = useTranslation()
const { inputs } = useNodeCrud<HumanInputNodeType>(id, payload)
const [showGeneratedForm, setShowGeneratedForm] = useState(false)
const [formData, setFormData] = useState<HumanInputFormData | null>(null)
const [requiredInputs, setRequiredInputs] = useState<Record<string, string>>({})
const generatedInputs = useMemo(() => {
const defaultInputs = inputs.inputs.reduce((acc, input) => {
if (input.default.type === 'variable') {
acc.push(`{{#${input.default.selector.join('.')}#}}`)
}
return acc
}, [] as string[])
const allInputs = getInputVars([...defaultInputs, inputs.form_content || '']).filter(item => !isOutput(item.value_selector || []))
return allInputs
}, [getInputVars, inputs.form_content, inputs.inputs])
const forms = useMemo(() => {
const forms: FormProps[] = [{
label: t(`${i18nPrefix}.singleRun.label`, { ns: 'workflow' })!,
inputs: generatedInputs,
values: runInputData,
onChange: setRunInputData,
}]
return forms
}, [t, generatedInputs, runInputData, setRunInputData])
const getDependentVars = () => {
return generatedInputs.map((item) => {
// Guard against null/undefined variable to prevent app crash
if (!item.variable || typeof item.variable !== 'string')
return []
return item.variable.slice(1, -1).split('.')
}).filter(arr => arr.length > 0)
}
const appDetail = useAppStore(s => s.appDetail)
const appId = appDetail?.id
const isWorkflowMode = appDetail?.mode === AppModeEnum.WORKFLOW
const fetchURL = useMemo(() => {
if (!appId)
return ''
if (!isWorkflowMode) {
return `/apps/${appId}/advanced-chat/workflows/draft/human-input/nodes/${id}/form`
}
else {
return `/apps/${appId}/workflows/draft/human-input/nodes/${id}/form`
}
}, [appId, id, isWorkflowMode])
const handleFetchFormContent = useCallback(async (inputs: Record<string, string>) => {
if (!fetchURL)
return null
let requestParamsObj: Record<string, string> = {}
Object.keys(inputs).forEach((key) => {
if (inputs[key] === undefined) {
delete inputs[key]
}
})
requestParamsObj = { ...inputs }
const data = await fetchHumanInputNodeStepRunForm(fetchURL, { inputs: requestParamsObj! })
setFormData(data)
setRequiredInputs(requestParamsObj)
return data
}, [fetchURL])
const handleSubmitHumanInputForm = useCallback(async (formData: {
inputs: Record<string, string> | undefined
form_inputs: Record<string, string> | undefined
action: string
}) => {
await submitHumanInputNodeStepRunForm(fetchURL, {
inputs: requiredInputs,
form_inputs: formData.inputs,
action: formData.action,
})
}, [fetchURL, requiredInputs])
const handleShowGeneratedForm = async (formValue: Record<string, string>) => {
setShowGeneratedForm(true)
await handleFetchFormContent(formValue)
}
const handleHideGeneratedForm = () => {
setShowGeneratedForm(false)
}
return {
forms,
getDependentVars,
showGeneratedForm,
handleShowGeneratedForm,
handleHideGeneratedForm,
formData,
handleFetchFormContent,
handleSubmitHumanInputForm,
}
}
export default useSingleRunFormParams

View File

@ -1,74 +0,0 @@
import type { FC } from 'react'
import type { HumanInputNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
import {
RiMailSendFill,
RiRobot2Fill,
} from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { NodeSourceHandle } from '../_base/components/node-handle'
import { DeliveryMethodType } from './types'
const i18nPrefix = 'nodes.humanInput'
const Node: FC<NodeProps<HumanInputNodeType>> = (props) => {
const { t } = useTranslation()
const { data } = props
const deliveryMethods = data.delivery_methods
const userActions = data.user_actions
return (
<>
{deliveryMethods.length > 0 && (
<div className="space-y-0.5 py-1">
<div className="system-2xs-medium-uppercase px-2.5 py-0.5 text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.title`, { ns: 'workflow' })}</div>
<div className="space-y-0.5 px-2.5">
{deliveryMethods.map(method => (
<div key={method.type} className="flex items-center gap-1 rounded-[6px] bg-workflow-block-parma-bg p-1">
{method.type === DeliveryMethodType.WebApp && (
<div className="rounded-[4px] border border-divider-regular bg-components-icon-bg-indigo-solid p-0.5">
<RiRobot2Fill className="h-3.5 w-3.5 text-text-primary-on-surface" />
</div>
)}
{method.type === DeliveryMethodType.Email && (
<div className="rounded-[4px] border border-divider-regular bg-components-icon-bg-blue-solid p-0.5">
<RiMailSendFill className="h-3.5 w-3.5 text-text-primary-on-surface" />
</div>
)}
<span className="system-xs-regular capitalize text-text-secondary">{method.type}</span>
</div>
))}
</div>
</div>
)}
<div className="space-y-0.5 py-1">
{userActions.length > 0 && (
<>
{userActions.map(userAction => (
<div key={userAction.id} className="relative flex flex-row-reverse items-center px-4 py-1">
<span className="system-xs-semibold-uppercase truncate text-text-secondary">{userAction.id}</span>
<NodeSourceHandle
{...props}
handleId={userAction.id}
handleClassName="!top-1/2 !-right-[9px] !-translate-y-1/2"
/>
</div>
))}
</>
)}
<div className="relative flex flex-row-reverse items-center px-4 py-1">
<div className="system-xs-semibold-uppercase truncate text-text-secondary">Timeout</div>
<NodeSourceHandle
{...props}
handleId="__timeout"
handleClassName="!top-1/2 !-right-[9px] !-translate-y-1/2"
/>
</div>
</div>
</>
)
}
export default React.memo(Node)

View File

@ -1,251 +0,0 @@
import type { FC } from 'react'
import type { HumanInputNodeType } from './types'
import type { NodePanelProps, Var } from '@/app/components/workflow/types'
import {
RiAddLine,
RiClipboardLine,
RiCollapseDiagonalLine,
RiExpandDiagonalLine,
RiEyeLine,
} from '@remixicon/react'
import { useBoolean } from 'ahooks'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { useStore } from '@/app/components/workflow/store'
import { VarType } from '@/app/components/workflow/types'
import { cn } from '@/utils/classnames'
import DeliveryMethod from './components/delivery-method'
import FormContent from './components/form-content'
import FormContentPreview from './components/form-content-preview'
import TimeoutInput from './components/timeout'
import UserActionItem from './components/user-action'
import useConfig from './hooks/use-config'
import { UserActionButtonType } from './types'
const i18nPrefix = 'nodes.humanInput'
const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleDeliveryMethodChange,
handleUserActionAdd,
handleUserActionChange,
handleUserActionDelete,
handleTimeoutChange,
handleFormContentChange,
handleFormInputsChange,
handleFormInputItemRename,
handleFormInputItemRemove,
editorKey,
structuredOutputCollapsed,
setStructuredOutputCollapsed,
} = useConfig(id, data)
const { availableVars, availableNodesWithParent } = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
},
})
const [isExpandFormContent, {
toggle: toggleExpandFormContent,
}] = useBoolean(false)
const nodePanelWidth = useStore(state => state.nodePanelWidth)
const [isPreview, {
toggle: togglePreview,
setFalse: hidePreview,
}] = useBoolean(false)
const onAddUseAction = useCallback(() => {
const index = inputs.user_actions.length + 1
handleUserActionAdd({
id: `action_${index}`,
title: `Button Text ${index}`,
button_style: UserActionButtonType.Default,
})
}, [handleUserActionAdd, inputs.user_actions.length])
return (
<div className="py-2">
{/* delivery methods */}
<DeliveryMethod
nodeId={id}
value={inputs.delivery_methods || []}
formContent={inputs.form_content}
formInputs={inputs.inputs}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
onChange={handleDeliveryMethodChange}
readonly={readOnly}
/>
<div className="px-4 py-2">
<Divider className="!my-0 !h-px !bg-divider-subtle" />
</div>
{/* form content */}
<div
className={cn('px-4 py-2', isExpandFormContent && 'fixed bottom-[8px] right-[4px] top-[244px] z-10 flex flex-col rounded-b-2xl bg-components-panel-bg')}
style={{
width: isExpandFormContent ? nodePanelWidth : '100%',
}}
>
<div className="mb-1 flex shrink-0 items-center justify-between">
<div className="flex h-6 items-center gap-0.5">
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.formContent.title`, { ns: 'workflow' })}</div>
<Tooltip
popupContent={t(`${i18nPrefix}.formContent.tooltip`, { ns: 'workflow' })}
/>
</div>
{!readOnly && (
<div className="flex items-center ">
<Button
variant="ghost"
size="small"
className={cn(
'flex items-center space-x-1 px-2',
isPreview && 'bg-state-accent-active text-text-accent',
)}
onClick={togglePreview}
>
<RiEyeLine className="size-3.5" />
<div className="system-xs-medium">{t(`${i18nPrefix}.formContent.preview`, { ns: 'workflow' })}</div>
</Button>
<div className="mx-2 h-3 w-px bg-divider-regular"></div>
<div className="flex items-center space-x-1">
<div
className="flex size-6 cursor-pointer items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
onClick={() => {
copy(inputs.form_content)
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
}}
>
<RiClipboardLine className="h-4 w-4 text-text-secondary" />
</div>
<div className={cn('flex size-6 cursor-pointer items-center justify-center rounded-md text-text-secondary hover:bg-components-button-ghost-bg-hover', isExpandFormContent && 'bg-state-accent-active text-text-accent')} onClick={toggleExpandFormContent}>
{isExpandFormContent ? <RiCollapseDiagonalLine className="h-4 w-4" /> : <RiExpandDiagonalLine className="h-4 w-4" />}
</div>
</div>
</div>
)}
</div>
<FormContent
editorKey={editorKey}
nodeId={id}
value={inputs.form_content}
onChange={handleFormContentChange}
formInputs={inputs.inputs}
onFormInputsChange={handleFormInputsChange}
onFormInputItemRename={handleFormInputItemRename}
onFormInputItemRemove={handleFormInputItemRemove}
isExpand={isExpandFormContent}
availableVars={availableVars}
availableNodes={availableNodesWithParent}
readonly={readOnly}
/>
</div>
{/* user actions */}
<div className="px-4 py-2">
<div className="mb-1 flex items-center justify-between">
<div className="flex items-center gap-0.5">
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.userActions.title`, { ns: 'workflow' })}</div>
<Tooltip
popupContent={t(`${i18nPrefix}.userActions.tooltip`, { ns: 'workflow' })}
/>
</div>
{!readOnly && (
<div className="flex items-center px-1">
<ActionButton
onClick={onAddUseAction}
>
<RiAddLine className="h-4 w-4" />
</ActionButton>
</div>
)}
</div>
{!inputs.user_actions.length && (
<div className="system-xs-regular flex items-center justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary">{t(`${i18nPrefix}.userActions.emptyTip`, { ns: 'workflow' })}</div>
)}
{inputs.user_actions.length > 0 && (
<div className="space-y-2">
{inputs.user_actions.map((action, index) => (
<UserActionItem
key={index}
data={action}
onChange={data => handleUserActionChange(index, data)}
onDelete={handleUserActionDelete}
readonly={readOnly}
/>
))}
</div>
)}
</div>
<div className="px-4 py-2">
<Divider className="!my-0 !h-px !bg-divider-subtle" />
</div>
{/* timeout */}
<div className="flex items-center justify-between px-4 py-2">
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.timeout.title`, { ns: 'workflow' })}</div>
<TimeoutInput
timeout={inputs.timeout}
unit={inputs.timeout_unit}
onChange={handleTimeoutChange}
readonly={readOnly}
/>
</div>
{/* output vars */}
<Split />
<OutputVars
collapsed={structuredOutputCollapsed}
onCollapse={setStructuredOutputCollapsed}
>
{
inputs.inputs.map(input => (
<VarItem
key={input.output_variable_name}
name={input.output_variable_name}
type={VarType.string}
description="Form input value"
/>
))
}
<VarItem
name="__action_id"
type="string"
description="Action ID user triggered"
/>
<VarItem
name="__rendered_content"
type="string"
description="Rendered content"
/>
</OutputVars>
{isPreview && (
<FormContentPreview
content={inputs.form_content}
formInputs={inputs.inputs}
userActions={inputs.user_actions}
onClose={hidePreview}
/>
)}
</div>
)
}
export default React.memo(Panel)

View File

@ -1,72 +0,0 @@
import type {
CommonNodeType,
InputVarType,
ValueSelector,
} from '@/app/components/workflow/types'
export type HumanInputNodeType = CommonNodeType & {
delivery_methods: DeliveryMethod[]
form_content: string
inputs: FormInputItem[]
user_actions: UserAction[]
timeout: number
timeout_unit: 'hour' | 'day'
}
export enum DeliveryMethodType {
WebApp = 'webapp',
Email = 'email',
Slack = 'slack',
Teams = 'teams',
Discord = 'discord',
}
export type Recipient = {
type: 'member' | 'external'
email?: string
user_id?: string
}
export type RecipientData = {
whole_workspace: boolean
items: Recipient[]
}
export type EmailConfig = {
recipients: RecipientData
subject: string
body: string
debug_mode: boolean
}
export type DeliveryMethod = {
id: string
type: DeliveryMethodType
enabled: boolean
config?: EmailConfig
}
export enum UserActionButtonType {
Primary = 'primary',
Default = 'default',
Accent = 'accent',
Ghost = 'ghost',
}
export type UserAction = {
id: string
title: string
button_style: UserActionButtonType
}
export type FormInputItemDefault = {
selector: ValueSelector
type: 'variable' | 'constant'
value: string
}
export type FormInputItem = {
type: InputVarType
output_variable_name: string
default: FormInputItemDefault
}

View File

@ -1,3 +0,0 @@
export const isOutput = (valueSelector: string[]) => {
return valueSelector[0] === '$output'
}

View File

@ -94,8 +94,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
handleModelChanged(model)
}
})()
}, [handleCompletionParamsChange, handleModelChanged, inputs.model.completion_params, t])
}, [inputs.model.completion_params])
return (
<div className="mt-2">
<div className="space-y-4 px-4 pb-4">

View File

@ -4,14 +4,12 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import { cn } from '@/utils/classnames'
type PlaceholderProps = {
disableVariableInsertion?: boolean
hideBadge?: boolean
}
const Placeholder = ({ disableVariableInsertion = false, hideBadge = false }: PlaceholderProps) => {
const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
@ -25,10 +23,7 @@ const Placeholder = ({ disableVariableInsertion = false, hideBadge = false }: Pl
return (
<div
className={cn(
'pointer-events-auto flex h-full w-full cursor-text px-2',
!hideBadge ? 'items-center' : 'items-start py-1',
)}
className="pointer-events-auto flex h-full w-full cursor-text items-center px-2"
onClick={(e) => {
e.stopPropagation()
handleInsert('')
@ -52,13 +47,11 @@ const Placeholder = ({ disableVariableInsertion = false, hideBadge = false }: Pl
</>
)}
</div>
{!hideBadge && (
<Badge
className="shrink-0"
text="String"
uppercase={false}
/>
)}
<Badge
className="shrink-0"
text="String"
uppercase={false}
/>
</div>
)
}

View File

@ -18,7 +18,7 @@ import {
useStore,
useWorkflowStore,
} from '../../store'
import { BlockEnum, WorkflowRunningStatus } from '../../types'
import { BlockEnum } from '../../types'
import ConversationVariableModal from './conversation-variable-modal'
import Empty from './empty'
import { useChat } from './hooks'
@ -84,9 +84,7 @@ const ChatWrapper = (
suggestedQuestions,
handleSend,
handleRestart,
handleSwitchSibling,
handleSubmitHumanInputForm,
getHumanInputNodeData,
setTargetMessageId,
} = useChat(
config,
{
@ -123,22 +121,6 @@ const ChatWrapper = (
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
}, [chatList, doSend])
const doSwitchSibling = useCallback((siblingMessageId: string) => {
handleSwitchSibling(siblingMessageId, {
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
})
}, [handleSwitchSibling, appDetail])
const doHumanInputFormSubmit = useCallback(async (formToken: string, formData: any) => {
// Handle human input form submission
await handleSubmitHumanInputForm(formToken, formData)
}, [handleSubmitHumanInputForm])
const inputDisabled = useMemo(() => {
const latestMessage = chatList[chatList.length - 1]
return latestMessage?.isAnswer && (latestMessage.workflowProcess?.status === WorkflowRunningStatus.Paused)
}, [chatList])
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {
if (v.type === EVENT_WORKFLOW_STOP)
@ -186,8 +168,6 @@ const ChatWrapper = (
inputsForm={(startVariables || []) as any}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
onHumanInputFormSubmit={doHumanInputFormSubmit}
getHumanInputNodeData={getHumanInputNodeData}
chatNode={(
<>
{showInputsFieldsPanel && <UserInput />}
@ -202,9 +182,7 @@ const ChatWrapper = (
suggestedQuestions={suggestedQuestions}
showPromptLog
chatAnswerContainerInner="!pr-2"
switchSibling={doSwitchSibling}
inputDisabled={inputDisabled}
hideAvatar
switchSibling={setTargetMessageId}
/>
{showConversationVariableModal && (
<ConversationVariableModal

View File

@ -5,7 +5,6 @@ import type {
Inputs,
} from '@/app/components/base/chat/types'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { IOtherOptions } from '@/service/base'
import { uniqBy } from 'es-toolkit/compat'
import { produce, setAutoFreeze } from 'immer'
import {
@ -16,7 +15,6 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import {
getProcessedInputs,
processOpeningStatement,
@ -27,12 +25,7 @@ import {
getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils'
import { useToastContext } from '@/app/components/base/toast'
import {
CUSTOM_NODE,
} from '@/app/components/workflow/constants'
import { sseGet } from '@/service/base'
import { useInvalidAllLastRun } from '@/service/use-workflow'
import { submitHumanInputForm } from '@/service/workflow'
import { TransferMethod } from '@/types/app'
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../constants'
import {
@ -65,7 +58,6 @@ export const useChat = (
const taskIdRef = useRef('')
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
const workflowEventsAbortControllerRef = useRef<AbortController | null>(null)
const configsMap = useHooksStore(s => s.configsMap)
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
@ -75,7 +67,6 @@ export const useChat = (
setIterTimes,
setLoopTimes,
} = workflowStore.getState()
const store = useStoreApi()
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
@ -140,29 +131,6 @@ export const useChat = (
})
}, [])
type UpdateChatTreeNode = {
(id: string, fields: Partial<ChatItemInTree>): void
(id: string, update: (node: ChatItemInTree) => void): void
}
const updateChatTreeNode: UpdateChatTreeNode = useCallback((
id: string,
fieldsOrUpdate: Partial<ChatItemInTree> | ((node: ChatItemInTree) => void),
) => {
const nextState = produceChatTreeNode(id, (node) => {
if (typeof fieldsOrUpdate === 'function') {
fieldsOrUpdate(node)
}
else {
Object.keys(fieldsOrUpdate).forEach((key) => {
(node as any)[key] = (fieldsOrUpdate as any)[key]
})
}
})
setChatTree(nextState)
chatTreeRef.current = nextState
}, [produceChatTreeNode])
const handleStop = useCallback(() => {
hasStopResponded.current = true
handleResponding(false)
@ -172,8 +140,6 @@ export const useChat = (
setLoopTimes(DEFAULT_LOOP_TIMES)
if (suggestedQuestionsAbortControllerRef.current)
suggestedQuestionsAbortControllerRef.current.abort()
if (workflowEventsAbortControllerRef.current)
workflowEventsAbortControllerRef.current.abort()
}, [handleResponding, setIterTimes, setLoopTimes, stopChat])
const handleRestart = useCallback(() => {
@ -240,10 +206,6 @@ export const useChat = (
return false
}
// Abort previous handleResume SSE connection if any
if (workflowEventsAbortControllerRef.current)
workflowEventsAbortControllerRef.current.abort()
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
const placeholderQuestionId = `question-${Date.now()}`
@ -281,8 +243,6 @@ export const useChat = (
isAnswer: true,
parentMessageId: questionItem.id,
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
humanInputFormDataList: [],
humanInputFilledFormDataList: [],
}
handleResponding(true)
@ -310,9 +270,6 @@ export const useChat = (
handleRun(
bodyParams,
{
getAbortController: (abortController) => {
workflowEventsAbortControllerRef.current = abortController
},
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
responseItem.content = responseItem.content + message
@ -338,38 +295,35 @@ export const useChat = (
})
},
async onCompleted(hasError?: boolean, errorMessage?: string) {
const { workflowRunningData } = workflowStore.getState()
handleResponding(false)
if (workflowRunningData?.result.status !== WorkflowRunningStatus.Paused) {
fetchInspectVars({})
invalidAllLastRun()
fetchInspectVars({})
invalidAllLastRun()
if (hasError) {
if (errorMessage) {
responseItem.content = errorMessage
responseItem.isError = true
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
}
return
if (hasError) {
if (errorMessage) {
responseItem.content = errorMessage
responseItem.isError = true
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
}
return
}
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
try {
const { data }: any = await onGetSuggestedQuestions(
responseItem.id,
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
)
setSuggestQuestions(data)
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (error) {
setSuggestQuestions([])
}
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
try {
const { data }: any = await onGetSuggestedQuestions(
responseItem.id,
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
)
setSuggestQuestions(data)
}
// eslint-disable-next-line unused-imports/no-unused-vars
catch (error) {
setSuggestQuestions([])
}
}
},
@ -391,29 +345,12 @@ export const useChat = (
onError() {
handleResponding(false)
},
onWorkflowStarted: ({ workflow_run_id, task_id, conversation_id, message_id }) => {
// If there are no streaming messages, we still need to set the conversation_id to avoid create a new conversation when regeneration in chat-flow.
if (conversation_id) {
conversationId.current = conversation_id
}
if (message_id && !hasSetResponseId) {
questionItem.id = `question-${message_id}`
responseItem.id = message_id
responseItem.parentMessageId = questionItem.id
hasSetResponseId = true
}
if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) {
handleResponding(true)
responseItem.workflowProcess.status = WorkflowRunningStatus.Running
}
else {
taskIdRef.current = task_id
responseItem.workflow_run_id = workflow_run_id
responseItem.workflowProcess = {
status: WorkflowRunningStatus.Running,
tracing: [],
}
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
taskIdRef.current = task_id
responseItem.workflow_run_id = workflow_run_id
responseItem.workflowProcess = {
status: WorkflowRunningStatus.Running,
tracing: [],
}
updateCurrentQAOnTree({
placeholderQuestionId,
@ -486,19 +423,10 @@ export const useChat = (
}
},
onNodeStarted: ({ data }) => {
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
if (currentIndex > -1) {
responseItem.workflowProcess!.tracing![currentIndex] = {
...data,
status: NodeRunningStatus.Running,
}
}
else {
responseItem.workflowProcess!.tracing!.push({
...data,
status: NodeRunningStatus.Running,
})
}
responseItem.workflowProcess!.tracing!.push({
...data,
status: NodeRunningStatus.Running,
} as any)
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
@ -571,383 +499,17 @@ export const useChat = (
})
}
},
onHumanInputRequired: ({ data }) => {
if (!responseItem.humanInputFormDataList) {
responseItem.humanInputFormDataList = [data]
}
else {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
if (currentFormIndex > -1) {
responseItem.humanInputFormDataList[currentFormIndex] = data
}
else {
responseItem.humanInputFormDataList.push(data)
}
}
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
if (currentTracingIndex > -1) {
responseItem.workflowProcess!.tracing[currentTracingIndex].status = NodeRunningStatus.Paused
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
}
},
onHumanInputFormFilled: ({ data }) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
}
if (!responseItem.humanInputFilledFormDataList) {
responseItem.humanInputFilledFormDataList = [data]
}
else {
responseItem.humanInputFilledFormDataList.push(data)
}
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onHumanInputFormTimeout: ({ data }) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = data.expiration_time
}
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onWorkflowPaused: ({ data: _data }) => {
responseItem.workflowProcess!.status = WorkflowRunningStatus.Paused
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
},
)
}, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, workflowStore, fetchInspectVars, invalidAllLastRun, config?.suggested_questions_after_answer?.enabled])
const handleSubmitHumanInputForm = async (formToken: string, formData: any) => {
await submitHumanInputForm(formToken, formData)
}
const getHumanInputNodeData = (nodeID: string) => {
const {
getNodes,
} = store.getState()
const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
const node = nodes.find(n => n.id === nodeID)
return node
}
const handleResume = useCallback((
messageId: string,
workflowRunId: string,
{
onGetSuggestedQuestions,
}: SendCallback,
) => {
// Re-subscribe to workflow events for the specific message
const url = `/workflow/${workflowRunId}/events?include_state_snapshot=true`
const otherOptions: IOtherOptions = {
getAbortController: (abortController) => {
workflowEventsAbortControllerRef.current = abortController
},
onData: (message: string, _isFirstMessage: boolean, { conversationId: newConversationId, messageId: msgId, taskId }: any) => {
updateChatTreeNode(messageId, (responseItem) => {
responseItem.content = responseItem.content + message
if (msgId)
responseItem.id = msgId
})
if (newConversationId)
conversationId.current = newConversationId
if (taskId)
taskIdRef.current = taskId
},
async onCompleted(hasError?: boolean) {
const { workflowRunningData } = workflowStore.getState()
handleResponding(false)
if (workflowRunningData?.result.status !== WorkflowRunningStatus.Paused) {
fetchInspectVars({})
invalidAllLastRun()
if (hasError)
return
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
try {
const { data }: any = await onGetSuggestedQuestions(
messageId,
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
)
setSuggestQuestions(data)
}
catch {
setSuggestQuestions([])
}
}
}
},
onMessageEnd: (messageEnd) => {
updateChatTreeNode(messageId, (responseItem) => {
responseItem.citation = messageEnd.metadata?.retriever_resources || []
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
})
},
onMessageReplace: (messageReplace) => {
updateChatTreeNode(messageId, (responseItem) => {
responseItem.content = messageReplace.answer
})
},
onError() {
handleResponding(false)
},
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
handleResponding(true)
hasStopResponded.current = false
updateChatTreeNode(messageId, (responseItem) => {
if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) {
responseItem.workflowProcess.status = WorkflowRunningStatus.Running
}
else {
taskIdRef.current = task_id
responseItem.workflow_run_id = workflow_run_id
responseItem.workflowProcess = {
status: WorkflowRunningStatus.Running,
tracing: [],
}
}
})
},
onWorkflowFinished: ({ data: workflowFinishedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (responseItem.workflowProcess)
responseItem.workflowProcess.status = workflowFinishedData.status as WorkflowRunningStatus
})
},
onIterationStart: ({ data: iterationStartedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.workflowProcess)
return
if (!responseItem.workflowProcess.tracing)
responseItem.workflowProcess.tracing = []
responseItem.workflowProcess.tracing.push({
...iterationStartedData,
status: WorkflowRunningStatus.Running,
})
})
},
onIterationFinish: ({ data: iterationFinishedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.workflowProcess?.tracing)
return
const tracing = responseItem.workflowProcess.tracing
const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id
&& (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))!
if (iterationIndex > -1) {
tracing[iterationIndex] = {
...tracing[iterationIndex],
...iterationFinishedData,
status: WorkflowRunningStatus.Succeeded,
}
}
})
},
onNodeStarted: ({ data: nodeStartedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.workflowProcess)
return
if (!responseItem.workflowProcess.tracing)
responseItem.workflowProcess.tracing = []
const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id)
if (currentIndex > -1) {
responseItem.workflowProcess.tracing[currentIndex] = {
...nodeStartedData,
status: NodeRunningStatus.Running,
}
}
else {
if (nodeStartedData.iteration_id)
return
responseItem.workflowProcess.tracing.push({
...nodeStartedData,
status: WorkflowRunningStatus.Running,
})
}
})
},
onNodeFinished: ({ data: nodeFinishedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.workflowProcess?.tracing)
return
if (nodeFinishedData.iteration_id)
return
const currentIndex = responseItem.workflowProcess.tracing.findIndex((item) => {
if (!item.execution_metadata?.parallel_id)
return item.id === nodeFinishedData.id
return item.id === nodeFinishedData.id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata?.parallel_id)
})
if (currentIndex > -1)
responseItem.workflowProcess.tracing[currentIndex] = nodeFinishedData as any
})
},
onLoopStart: ({ data: loopStartedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.workflowProcess)
return
if (!responseItem.workflowProcess.tracing)
responseItem.workflowProcess.tracing = []
responseItem.workflowProcess.tracing.push({
...loopStartedData,
status: WorkflowRunningStatus.Running,
})
})
},
onLoopFinish: ({ data: loopFinishedData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.workflowProcess?.tracing)
return
const tracing = responseItem.workflowProcess.tracing
const loopIndex = tracing.findIndex(item => item.node_id === loopFinishedData.node_id
&& (item.execution_metadata?.parallel_id === loopFinishedData.execution_metadata?.parallel_id || item.parallel_id === loopFinishedData.execution_metadata?.parallel_id))!
if (loopIndex > -1) {
tracing[loopIndex] = {
...tracing[loopIndex],
...loopFinishedData,
status: WorkflowRunningStatus.Succeeded,
}
}
})
},
onHumanInputRequired: ({ data: humanInputRequiredData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (!responseItem.humanInputFormDataList) {
responseItem.humanInputFormDataList = [humanInputRequiredData]
}
else {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputRequiredData.node_id)
if (currentFormIndex > -1) {
responseItem.humanInputFormDataList[currentFormIndex] = humanInputRequiredData
}
else {
responseItem.humanInputFormDataList.push(humanInputRequiredData)
}
}
if (responseItem.workflowProcess?.tracing) {
const currentTracingIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === humanInputRequiredData.node_id)
if (currentTracingIndex > -1)
responseItem.workflowProcess.tracing[currentTracingIndex].status = NodeRunningStatus.Paused
}
})
},
onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
if (currentFormIndex > -1)
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
}
if (!responseItem.humanInputFilledFormDataList) {
responseItem.humanInputFilledFormDataList = [humanInputFilledFormData]
}
else {
responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData)
}
})
},
onHumanInputFormTimeout: ({ data: humanInputFormTimeoutData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFormTimeoutData.node_id)
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = humanInputFormTimeoutData.expiration_time
}
})
},
onWorkflowPaused: ({ data: workflowPausedData }) => {
const resumeUrl = `/workflow/${workflowPausedData.workflow_run_id}/events`
sseGet(
resumeUrl,
{},
otherOptions,
)
updateChatTreeNode(messageId, (responseItem) => {
responseItem.workflowProcess!.status = WorkflowRunningStatus.Paused
})
},
}
if (workflowEventsAbortControllerRef.current)
workflowEventsAbortControllerRef.current.abort()
sseGet(
url,
{},
otherOptions,
)
}, [updateChatTreeNode, handleResponding, workflowStore, fetchInspectVars, invalidAllLastRun, config?.suggested_questions_after_answer])
const handleSwitchSibling = useCallback((
siblingMessageId: string,
callbacks: SendCallback,
) => {
setTargetMessageId(siblingMessageId)
// Helper to find message in tree
const findMessageInTree = (nodes: ChatItemInTree[], targetId: string): ChatItemInTree | undefined => {
for (const node of nodes) {
if (node.id === targetId)
return node
if (node.children) {
const found = findMessageInTree(node.children, targetId)
if (found)
return found
}
}
return undefined
}
const targetMessage = findMessageInTree(chatTreeRef.current, siblingMessageId)
if (targetMessage?.workflow_run_id && targetMessage.humanInputFormDataList && targetMessage.humanInputFormDataList.length > 0) {
handleResume(
targetMessage.id,
targetMessage.workflow_run_id,
callbacks,
)
}
}, [handleResume])
}, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled, fetchInspectVars, invalidAllLastRun])
return {
conversationId: conversationId.current,
chatList,
setTargetMessageId,
handleSwitchSibling,
handleSend,
handleStop,
handleRestart,
handleResume,
handleSubmitHumanInputForm,
getHumanInputNodeData,
isResponding,
suggestedQuestions,
}

View File

@ -1,34 +0,0 @@
import type { HumanInputFilledFormData } from '@/types/workflow'
import ContentWrapper from '@/app/components/base/chat/chat/answer/human-input-content/content-wrapper'
import { SubmittedHumanInputContent } from '@/app/components/base/chat/chat/answer/human-input-content/submitted'
type HumanInputFilledFormListProps = {
humanInputFilledFormDataList: HumanInputFilledFormData[]
}
const HumanInputFilledFormList = ({
humanInputFilledFormDataList,
}: HumanInputFilledFormListProps) => {
return (
<div className="mt-3 flex flex-col gap-y-3 first:mt-0">
{
humanInputFilledFormDataList.map(formData => (
<ContentWrapper
key={formData.node_id}
nodeTitle={formData.node_title}
showExpandIcon
className="bg-components-panel-bg"
expanded
>
<SubmittedHumanInputContent
key={formData.node_id}
formData={formData}
/>
</ContentWrapper>
))
}
</div>
)
}
export default HumanInputFilledFormList

View File

@ -1,83 +0,0 @@
import type { DeliveryMethod } from '@/app/components/workflow/nodes/human-input/types'
import type { HumanInputFormData } from '@/types/workflow'
import { useCallback, useMemo } from 'react'
import { useStoreApi } from 'reactflow'
import ContentWrapper from '@/app/components/base/chat/chat/answer/human-input-content/content-wrapper'
import { UnsubmittedHumanInputContent } from '@/app/components/base/chat/chat/answer/human-input-content/unsubmitted'
import { CUSTOM_NODE } from '@/app/components/workflow/constants'
import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
type HumanInputFormListProps = {
humanInputFormDataList: HumanInputFormData[]
onHumanInputFormSubmit?: (formToken: string, formData: any) => Promise<void>
}
const HumanInputFormList = ({
humanInputFormDataList,
onHumanInputFormSubmit,
}: HumanInputFormListProps) => {
const store = useStoreApi()
const getHumanInputNodeData = useCallback((nodeID: string) => {
const {
getNodes,
} = store.getState()
const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
const node = nodes.find(n => n.id === nodeID)
return node
}, [store])
const deliveryMethodsConfig = useMemo((): Record<string, { showEmailTip: boolean, isEmailDebugMode: boolean, showDebugModeTip: boolean }> => {
if (!humanInputFormDataList.length)
return {}
return humanInputFormDataList.reduce((acc, formData) => {
const deliveryMethodsConfig = getHumanInputNodeData(formData.node_id)?.data.delivery_methods || []
if (!deliveryMethodsConfig.length) {
acc[formData.node_id] = {
showEmailTip: false,
isEmailDebugMode: false,
showDebugModeTip: false,
}
return acc
}
const isWebappEnabled = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.WebApp && method.enabled)
const isEmailEnabled = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.Email && method.enabled)
const isEmailDebugMode = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.Email && method.config?.debug_mode)
acc[formData.node_id] = {
showEmailTip: isEmailEnabled,
isEmailDebugMode,
showDebugModeTip: !isWebappEnabled,
}
return acc
}, {} as Record<string, { showEmailTip: boolean, isEmailDebugMode: boolean, showDebugModeTip: boolean }>)
}, [getHumanInputNodeData, humanInputFormDataList])
const filteredHumanInputFormDataList = useMemo(() => {
return humanInputFormDataList.filter(formData => formData.display_in_ui)
}, [humanInputFormDataList])
return (
<div className="flex flex-col gap-y-3">
{
filteredHumanInputFormDataList.map(formData => (
<ContentWrapper
key={formData.node_id}
nodeTitle={formData.node_title}
className="bg-components-panel-bg"
>
<UnsubmittedHumanInputContent
key={formData.node_id}
formData={formData}
showEmailTip={!!deliveryMethodsConfig[formData.node_id]?.showEmailTip}
isEmailDebugMode={!!deliveryMethodsConfig[formData.node_id]?.isEmailDebugMode}
showDebugModeTip={!!deliveryMethodsConfig[formData.node_id]?.showDebugModeTip}
onSubmit={onHumanInputFormSubmit}
/>
</ContentWrapper>
))
}
</div>
)
}
export default HumanInputFormList

View File

@ -44,18 +44,15 @@ const InputsPanel = ({ onRun }: Props) => {
const startVariables = startNode?.data.variables
const { checkInputsForm } = useCheckInputsForms()
const initialInputs = useMemo(() => {
const result = { ...inputs }
if (startVariables) {
startVariables.forEach((variable) => {
if (variable.default)
result[variable.variable] = variable.default
if (inputs[variable.variable] !== undefined)
result[variable.variable] = inputs[variable.variable]
})
}
return result
}, [inputs, startVariables])
const initialInputs = { ...inputs }
if (startVariables) {
startVariables.forEach((variable) => {
if (variable.default)
initialInputs[variable.variable] = variable.default
if (inputs[variable.variable] !== undefined)
initialInputs[variable.variable] = inputs[variable.variable]
})
}
const variables = useMemo(() => {
const data = startVariables || []
@ -100,7 +97,10 @@ const InputsPanel = ({ onRun }: Props) => {
}, [files, handleRun, initialInputs, onRun, variables, checkInputsForm])
const canRun = useMemo(() => {
return !(files?.some(item => (item.transfer_method as any) === TransferMethod.local_file && !item.upload_file_id))
if (files?.some(item => (item.transfer_method as any) === TransferMethod.local_file && !item.upload_file_id))
return false
return true
}, [files])
return (

View File

@ -12,7 +12,6 @@ import {
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { submitHumanInputForm } from '@/service/workflow'
import { cn } from '@/utils/classnames'
import Toast from '../../base/toast'
import {
@ -26,8 +25,6 @@ import {
WorkflowRunningStatus,
} from '../types'
import { formatWorkflowRunIdentifier } from '../utils'
import HumanInputFilledFormList from './human-input-filled-form-list'
import HumanInputFormList from './human-input-form-list'
import InputsPanel from './inputs-panel'
const WorkflowPreview = () => {
@ -40,8 +37,6 @@ const WorkflowPreview = () => {
const panelWidth = useStore(s => s.previewPanelWidth)
const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const humanInputFormDataList = useStore(s => s.workflowRunningData?.humanInputFormDataList)
const humanInputFilledFormDataList = useStore(s => s.workflowRunningData?.humanInputFilledFormDataList)
const [currentTab, setCurrentTab] = useState<string>(showInputsPanel ? 'INPUT' : 'TRACING')
const switchTab = async (tab: string) => {
@ -50,7 +45,7 @@ const WorkflowPreview = () => {
useEffect(() => {
if (showDebugAndPreviewPanel && showInputsPanel)
switchTab('INPUT')
setCurrentTab('INPUT')
}, [showDebugAndPreviewPanel, showInputsPanel])
useEffect(() => {
@ -65,8 +60,6 @@ const WorkflowPreview = () => {
if ((status === WorkflowRunningStatus.Succeeded || status === WorkflowRunningStatus.Failed) && !workflowRunningData.resultText && !workflowRunningData.result.files?.length)
switchTab('DETAIL')
if (status === WorkflowRunningStatus.Paused)
switchTab('RESULT')
}, [workflowRunningData])
const [isResizing, setIsResizing] = useState(false)
@ -101,10 +94,6 @@ const WorkflowPreview = () => {
}
}, [resize, stopResizing])
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: any) => {
await submitHumanInputForm(formToken, formData)
}, [])
return (
<div
className="relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
@ -185,21 +174,9 @@ const WorkflowPreview = () => {
<InputsPanel onRun={() => switchTab('RESULT')} />
)}
{currentTab === 'RESULT' && (
<div className="p-2">
{humanInputFormDataList && humanInputFormDataList.length > 0 && (
<HumanInputFormList
humanInputFormDataList={humanInputFormDataList}
onHumanInputFormSubmit={handleSubmitHumanInputForm}
/>
)}
{humanInputFilledFormDataList && humanInputFilledFormDataList.length > 0 && (
<HumanInputFilledFormList
humanInputFilledFormDataList={humanInputFilledFormDataList}
/>
)}
<>
<ResultText
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
isPaused={workflowRunningData?.result?.status === WorkflowRunningStatus.Paused}
outputs={workflowRunningData?.resultText}
allFiles={workflowRunningData?.result?.files}
error={workflowRunningData?.result?.error}
@ -221,7 +198,7 @@ const WorkflowPreview = () => {
<div>{t('operation.copy', { ns: 'common' })}</div>
</Button>
)}
</div>
</>
)}
{currentTab === 'DETAIL' && (
<ResultPanel

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { WorkflowRunDetailResponse } from '@/models/log'
import type { NodeTracing } from '@/types/workflow'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
@ -181,7 +182,6 @@ const RunPanel: FC<RunProps> = ({
steps={runDetail.total_steps}
exceptionCounts={runDetail.exceptions_count}
isListening={isListening}
workflowRunId={runDetail.id}
/>
)}
{!loading && currentTab === 'DETAIL' && !runDetail && isListening && (

View File

@ -50,9 +50,6 @@ const MetaData: FC<Props> = ({
{status === 'stopped' && (
<span>STOP</span>
)}
{status === 'paused' && (
<span>PENDING</span>
)}
</div>
</div>
<div className="flex">
@ -91,10 +88,10 @@ const MetaData: FC<Props> = ({
<div className="flex">
<div className="system-xs-regular w-[104px] shrink-0 truncate px-2 py-1.5 text-text-tertiary">{t('meta.tokens', { ns: 'runLog' })}</div>
<div className="system-xs-regular grow px-2 py-1.5 text-text-secondary">
{['running', 'paused'].includes(status) && (
<div className="my-1 h-2 w-[48px] animate-pulse rounded-sm bg-text-quaternary" />
{status === 'running' && (
<div className="my-1 h-2 w-[48px] rounded-sm bg-text-quaternary" />
)}
{!['running', 'paused'].includes(status) && (
{status !== 'running' && (
<span>{`${tokens || 0} Tokens`}</span>
)}
</div>

View File

@ -11,9 +11,8 @@ import {
RiAlertFill,
RiArrowRightSLine,
RiCheckboxCircleFill,
RiErrorWarningFill,
RiErrorWarningLine,
RiLoader2Line,
RiPauseCircleFill,
} from '@remixicon/react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -145,7 +144,7 @@ const NodePanel: FC<Props> = ({
{nodeInfo.title}
</div>
</Tooltip>
{!['running', 'paused'].includes(nodeInfo.status) && !hideInfo && (
{nodeInfo.status !== 'running' && !hideInfo && (
<div className="system-xs-regular shrink-0 text-text-tertiary">
{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}
{`${getTime(nodeInfo.elapsed_time || 0)}`}
@ -155,14 +154,11 @@ const NodePanel: FC<Props> = ({
<RiCheckboxCircleFill className="ml-2 h-3.5 w-3.5 shrink-0 text-text-success" />
)}
{nodeInfo.status === 'failed' && (
<RiErrorWarningFill className="ml-2 h-3.5 w-3.5 shrink-0 text-text-destructive" />
<RiErrorWarningLine className="ml-2 h-3.5 w-3.5 shrink-0 text-text-warning" />
)}
{nodeInfo.status === 'stopped' && (
<RiAlertFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
)}
{nodeInfo.status === 'paused' && (
<RiPauseCircleFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
)}
{nodeInfo.status === 'exception' && (
<RiAlertFill className={cn('ml-2 h-4 w-4 shrink-0 text-text-warning-secondary', inMessage && 'h-3.5 w-3.5')} />
)}
@ -233,11 +229,6 @@ const NodePanel: FC<Props> = ({
{nodeInfo.error}
</StatusContainer>
)}
{(nodeInfo.status === 'paused') && (
<StatusContainer status="paused">
<div className="system-xs-regular text-text-warning">{t('nodes.humanInput.log.reasonContent', { ns: 'workflow' })}</div>
</StatusContainer>
)}
</div>
{nodeInfo.inputs && (
<div className={cn('mb-1')}>

View File

@ -41,7 +41,6 @@ export type ResultPanelProps = {
exceptionCounts?: number
execution_metadata?: any
isListening?: boolean
workflowRunId?: string
handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
@ -68,7 +67,6 @@ const ResultPanel: FC<ResultPanelProps> = ({
exceptionCounts,
execution_metadata,
isListening = false,
workflowRunId,
handleShowIterationResultList,
handleShowLoopResultList,
onShowRetryDetail,
@ -91,7 +89,6 @@ const ResultPanel: FC<ResultPanelProps> = ({
error={error}
exceptionCounts={exceptionCounts}
isListening={isListening}
workflowRunId={workflowRunId}
/>
</div>
<div className="px-4">

View File

@ -9,7 +9,6 @@ import StatusContainer from '@/app/components/workflow/run/status-container'
type ResultTextProps = {
isRunning?: boolean
isPaused?: boolean
outputs?: any
error?: string
onClick?: () => void
@ -18,7 +17,6 @@ type ResultTextProps = {
const ResultText: FC<ResultTextProps> = ({
isRunning,
isPaused,
outputs,
error,
onClick,
@ -39,7 +37,7 @@ const ResultText: FC<ResultTextProps> = ({
</StatusContainer>
</div>
)}
{!isPaused && !isRunning && !outputs && !error && !allFiles?.length && (
{!isRunning && !outputs && !error && !allFiles?.length && (
<div className="mt-[120px] flex flex-col items-center px-4 py-2 text-[13px] leading-[18px] text-gray-500">
<ImageIndentLeft className="h-6 w-6 text-gray-400" />
<div className="mr-2">{t('resultEmpty.title', { ns: 'runLog' })}</div>

View File

@ -28,9 +28,9 @@ const StatusContainer: FC<Props> = ({
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] text-text-warning',
status === 'failed' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'failed' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(240,68,56,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
(status === 'stopped' || status === 'paused') && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
(status === 'stopped' || status === 'paused') && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
(status === 'stopped' || status === 'paused') && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
status === 'stopped' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'stopped' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
status === 'exception' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'exception' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',

View File

@ -1,11 +1,9 @@
'use client'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Indicator from '@/app/components/header/indicator'
import StatusContainer from '@/app/components/workflow/run/status-container'
import { useDocLink } from '@/context/i18n'
import { useWorkflowPausedDetails } from '@/service/use-log'
import { cn } from '@/utils/classnames'
type ResultProps = {
@ -15,7 +13,6 @@ type ResultProps = {
error?: string
exceptionCounts?: number
isListening?: boolean
workflowRunId?: string
}
const StatusPanel: FC<ResultProps> = ({
@ -25,45 +22,9 @@ const StatusPanel: FC<ResultProps> = ({
error,
exceptionCounts,
isListening = false,
workflowRunId,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { data: pausedDetails } = useWorkflowPausedDetails({
workflowRunId: workflowRunId || '',
enabled: status === 'paused',
})
const pausedReasons = useMemo(() => {
const reasons: string[] = []
if (!pausedDetails)
return reasons
const hasHumanInputNode = pausedDetails.paused_nodes.some(
node => node.pause_type.type === 'human_input',
)
if (hasHumanInputNode) {
reasons.push(t('nodes.humanInput.log.reasonContent', { ns: 'workflow' }))
}
return reasons
}, [pausedDetails, t])
const pausedInputURLs = useMemo(() => {
const inputURLs: string[] = []
if (!pausedDetails)
return inputURLs
const { paused_nodes } = pausedDetails
const hasHumanInputNode = paused_nodes.some(
node => node.pause_type.type === 'human_input',
)
if (hasHumanInputNode) {
paused_nodes.forEach((node) => {
if (node.pause_type.type === 'human_input') {
inputURLs.push(node.pause_type.backstage_input_url)
}
})
}
return inputURLs
}, [pausedDetails])
return (
<StatusContainer status={status}>
@ -80,7 +41,7 @@ const StatusPanel: FC<ResultProps> = ({
status === 'succeeded' && 'text-util-colors-green-green-600',
status === 'partial-succeeded' && 'text-util-colors-green-green-600',
status === 'failed' && 'text-util-colors-red-red-600',
(status === 'stopped' || status === 'paused') && 'text-util-colors-warning-warning-600',
status === 'stopped' && 'text-util-colors-warning-warning-600',
status === 'running' && 'text-util-colors-blue-light-blue-light-600',
)}
>
@ -120,21 +81,15 @@ const StatusPanel: FC<ResultProps> = ({
<span>STOP</span>
</>
)}
{status === 'paused' && (
<>
<Indicator color="yellow" />
<span>PENDING</span>
</>
)}
</div>
</div>
<div className="max-w-[152px] flex-[33%]">
<div className="system-2xs-medium-uppercase mb-1 text-text-tertiary">{t('resultPanel.time', { ns: 'runLog' })}</div>
<div className="system-sm-medium flex items-center gap-1 text-text-secondary">
{(status === 'running' || status === 'paused') && (
<div className="h-2 w-16 animate-pulse rounded-sm bg-text-quaternary" />
{status === 'running' && (
<div className="h-2 w-16 rounded-sm bg-text-quaternary" />
)}
{status !== 'running' && status !== 'paused' && (
{status !== 'running' && (
<span>{time ? `${time?.toFixed(3)}s` : '-'}</span>
)}
</div>
@ -142,10 +97,10 @@ const StatusPanel: FC<ResultProps> = ({
<div className="flex-[33%]">
<div className="system-2xs-medium-uppercase mb-1 text-text-tertiary">{t('resultPanel.tokens', { ns: 'runLog' })}</div>
<div className="system-sm-medium flex items-center gap-1 text-text-secondary">
{(status === 'running' || status === 'paused') && (
<div className="h-2 w-20 animate-pulse rounded-sm bg-text-quaternary" />
{status === 'running' && (
<div className="h-2 w-20 rounded-sm bg-text-quaternary" />
)}
{status !== 'running' && status !== 'paused' && (
{status !== 'running' && (
<span>{`${tokens || 0} Tokens`}</span>
)}
</div>
@ -194,40 +149,6 @@ const StatusPanel: FC<ResultProps> = ({
</>
)
}
{status === 'paused' && (
<>
<div className="my-2 h-[0.5px] bg-divider-deep" />
<div className="system-xs-medium flex flex-col gap-y-2">
<div className="flex flex-col gap-y-0.5">
<div className="system-2xs-medium-uppercase text-text-tertiary">{t('nodes.humanInput.log.reason', { ns: 'workflow' })}</div>
{
pausedReasons.length > 0
? pausedReasons.map(reason => (
<div className="system-xs-medium truncate text-text-secondary" key={reason}>{reason}</div>
))
: (
<div className="h-2 w-20 animate-pulse rounded-sm bg-text-quaternary" />
)
}
</div>
{pausedInputURLs.length > 0 && (
<div className="flex flex-col gap-y-0.5">
<div className="system-2xs-medium-uppercase text-text-tertiary">{t('nodes.humanInput.log.backstageInputURL', { ns: 'workflow' })}</div>
{pausedInputURLs.map(url => (
<a
key={url}
href={url}
target="_blank"
className="system-xs-medium text-text-accent"
>
{url}
</a>
))}
</div>
)}
</div>
</>
)}
</StatusContainer>
)
}

View File

@ -1,59 +0,0 @@
import type { NodeTracing } from '@/types/workflow'
import { BlockEnum } from '@/app/components/workflow/types'
/**
* Format human-input nodes to ensure only the latest status is kept for each node.
* Human-input nodes can have multiple log entries as their status changes
* (e.g., running -> paused -> succeeded/failed).
* This function keeps only the entry with the latest index for each unique node_id.
*/
const formatHumanInputNode = (list: NodeTracing[]): NodeTracing[] => {
// Group human-input nodes by node_id
const humanInputNodeMap = new Map<string, NodeTracing>()
// Track which node_ids are human-input type
const humanInputNodeIds = new Set<string>()
// First pass: identify human-input nodes and keep the one with the highest index
list.forEach((item) => {
if (item.node_type === BlockEnum.HumanInput) {
humanInputNodeIds.add(item.node_id)
const existingNode = humanInputNodeMap.get(item.node_id)
if (!existingNode || item.index > existingNode.index) {
humanInputNodeMap.set(item.node_id, item)
}
}
})
// If no human-input nodes, return the list as is
if (humanInputNodeIds.size === 0)
return list
// Second pass: filter the list to remove duplicate human-input nodes
// and keep only the latest one for each node_id
const result: NodeTracing[] = []
const addedHumanInputNodeIds = new Set<string>()
list.forEach((item) => {
if (item.node_type === BlockEnum.HumanInput) {
// Only add the human-input node with the highest index
if (!addedHumanInputNodeIds.has(item.node_id)) {
const latestNode = humanInputNodeMap.get(item.node_id)
if (latestNode) {
result.push(latestNode)
addedHumanInputNodeIds.add(item.node_id)
}
}
// Skip duplicate human-input nodes
}
else {
// Keep all non-human-input nodes
result.push(item)
}
})
return result
}
export default formatHumanInputNode

View File

@ -2,7 +2,6 @@ import type { NodeTracing } from '@/types/workflow'
import { cloneDeep } from 'es-toolkit/object'
import { BlockEnum } from '../../../types'
import formatAgentNode from './agent'
import formatHumanInputNode from './human-input'
import { addChildrenToIterationNode } from './iteration'
import { addChildrenToLoopNode } from './loop'
import formatParallelNode from './parallel'
@ -84,8 +83,7 @@ const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
* Because Handle struct node will put the node in different
*/
const formattedAgentList = formatAgentNode(allItems)
const formattedHumanInputList = formatHumanInputNode(formattedAgentList) // Keep only latest status for human-input nodes
const formattedRetryList = formatRetryNode(formattedHumanInputList) // retry one node
const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node
// would change the structure of the list. Iteration and parallel can include each other.
const formattedLoopAndIterationList = formatIterationAndLoopNode(formattedRetryList, t)
const formattedParallelList = formatParallelNode(formattedLoopAndIterationList, t)

View File

@ -9,8 +9,6 @@ import type { FileUploadConfigResponse } from '@/models/common'
type PreviewRunningData = WorkflowRunningData & {
resultTabActive?: boolean
resultText?: string
// human input form schema or data cached when node is in 'Paused' status
extraContentAndFormData?: Record<string, any>
}
export type WorkflowSliceShape = {

View File

@ -17,13 +17,7 @@ import type { VarType as VarKindType } from '@/app/components/workflow/nodes/too
import type { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import type { SchemaTypeDefinition } from '@/service/use-common'
import type { Resolution, TransferMethod } from '@/types/app'
import type {
FileResponse,
HumanInputFilledFormData,
HumanInputFormData,
NodeTracing,
PanelProps,
} from '@/types/workflow'
import type { FileResponse, NodeTracing, PanelProps } from '@/types/workflow'
export enum BlockEnum {
Start = 'start',
@ -49,7 +43,6 @@ export enum BlockEnum {
Loop = 'loop',
LoopStart = 'loop-start',
LoopEnd = 'loop-end',
HumanInput = 'human-input',
DataSource = 'datasource',
DataSourceEmpty = 'datasource-empty',
KnowledgeBase = 'knowledge-index',
@ -198,6 +191,7 @@ export enum InputVarType {
paragraph = 'paragraph',
select = 'select',
number = 'number',
checkbox = 'checkbox',
url = 'url',
files = 'files',
json = 'json', // obj, array
@ -207,7 +201,6 @@ export enum InputVarType {
singleFile = 'file',
multiFiles = 'file-list',
loop = 'loop', // loop input
checkbox = 'checkbox',
}
export type InputVar = {
@ -357,7 +350,6 @@ export enum WorkflowRunningStatus {
Succeeded = 'succeeded',
Failed = 'failed',
Stopped = 'stopped',
Paused = 'paused',
}
export enum WorkflowVersion {
@ -375,7 +367,6 @@ export enum NodeRunningStatus {
Exception = 'exception',
Retry = 'retry',
Stopped = 'stopped',
Paused = 'paused',
}
export type OnNodeAdd = (
@ -435,8 +426,6 @@ export type WorkflowRunningData = {
exceptions_count?: number
}
tracing?: NodeTracing[]
humanInputFormDataList?: HumanInputFormData[]
humanInputFilledFormDataList?: HumanInputFilledFormData[]
}
export type HistoryWorkflowData = {

View File

@ -1,5 +1,4 @@
import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types'
import type {
Edge,
@ -346,76 +345,6 @@ const buildIfElseWithPorts = (
}
}
/**
* Build Human Input node with ELK native Ports for multiple branches
* Handles user actions as branches with __timeout as the last fixed branch
*/
const buildHumanInputWithPorts = (
humanInputNode: Node,
edges: Edge[],
): { node: ElkNodeShape, portMap: Map<string, string> } | null => {
const childEdges = edges.filter(edge => edge.source === humanInputNode.id)
if (childEdges.length <= 1)
return null
// Sort child edges: user actions first (by order), then __timeout last
const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const userActions = (humanInputNode.data as HumanInputNodeType).user_actions || []
const isATimeout = handleA === '__timeout'
const isBTimeout = handleB === '__timeout'
// __timeout should always be last
if (isATimeout)
return 1
if (isBTimeout)
return -1
// Sort by user_actions order
const indexA = userActions.findIndex(action => action.id === handleA)
const indexB = userActions.findIndex(action => action.id === handleB)
if (indexA !== -1 && indexB !== -1)
return indexA - indexB
}
return 0
})
// Create ELK ports for each branch
const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({
id: `${humanInputNode.id}-port-${edge.sourceHandle || index}`,
layoutOptions: {
'port.side': 'EAST',
'port.index': String(index),
},
}))
// Build port mapping: edge.id -> portId
const portMap = new Map<string, string>()
sortedChildEdges.forEach((edge, index) => {
const portId = `${humanInputNode.id}-port-${edge.sourceHandle || index}`
portMap.set(edge.id, portId)
})
return {
node: {
id: humanInputNode.id,
width: humanInputNode.width ?? DEFAULT_NODE_WIDTH,
height: humanInputNode.height ?? DEFAULT_NODE_HEIGHT,
ports,
layoutOptions: {
'elk.portConstraints': 'FIXED_ORDER',
},
},
portMap,
}
}
const normaliseBounds = (layout: LayoutResult): LayoutResult => {
const {
nodes,
@ -459,7 +388,7 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
// Track which edges have been processed for If/Else nodes with ports
const edgeToPortMap = new Map<string, string>()
// Build nodes with ports for If/Else and Human Input nodes
// Build nodes with ports for If/Else nodes
nodes.forEach((node) => {
if (node.data.type === BlockEnum.IfElse) {
const portsResult = buildIfElseWithPorts(node, edges)
@ -476,21 +405,6 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
elkNodes.push(toElkNode(node))
}
}
else if (node.data.type === BlockEnum.HumanInput) {
const portsResult = buildHumanInputWithPorts(node, edges)
if (portsResult) {
// Use node with ports
elkNodes.push(portsResult.node)
// Store port mappings for edges
portsResult.portMap.forEach((portId, edgeId) => {
edgeToPortMap.set(edgeId, portId)
})
}
else {
// No multiple branches, use normal node
elkNodes.push(toElkNode(node))
}
}
else {
elkNodes.push(toElkNode(node))
}

View File

@ -33,7 +33,6 @@ export const canRunBySingle = (nodeType: BlockEnum, isChildNode: boolean) => {
|| nodeType === BlockEnum.IfElse
|| nodeType === BlockEnum.VariableAggregator
|| nodeType === BlockEnum.Assigner
|| nodeType === BlockEnum.HumanInput
|| nodeType === BlockEnum.DataSource
|| nodeType === BlockEnum.TriggerSchedule
|| nodeType === BlockEnum.TriggerWebhook

View File

@ -68,7 +68,7 @@ const BaseCard = ({
handleId="target"
/>
{
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && (
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && (
<NodeSourceHandle
id={id}
data={data}

View File

@ -4,7 +4,6 @@ import IterationNode from './iteration/node'
import LoopNode from './loop/node'
import QuestionClassifierNode from './question-classifier/node'
// todo: add human-input node support
export const NodeComponentMap: Record<string, any> = {
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
[BlockEnum.IfElse]: IfElseNode,