mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
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:
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
]
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -27,7 +27,6 @@ export const WorkflowHistoryEvent = {
|
||||
NodeDelete: 'NodeDelete',
|
||||
EdgeDelete: 'EdgeDelete',
|
||||
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
|
||||
EdgeSourceHandleChange: 'EdgeSourceHandleChange',
|
||||
NodeAdd: 'NodeAdd',
|
||||
NodeResize: 'NodeResize',
|
||||
NoteAdd: 'NoteAdd',
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -116,7 +116,7 @@ export const useWorkflowVariables = () => {
|
||||
schemaTypeDefinitions,
|
||||
preferSchemaType,
|
||||
})
|
||||
}, [workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
|
||||
}, [workflowStore, getVarType, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
return {
|
||||
getNodeAvailableVars,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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>
|
||||
{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export const isOutput = (valueSelector: string[]) => {
|
||||
return valueSelector[0] === '$output'
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')}>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)]',
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user