feat: Add @agent icon and implement agent alias variables in workflow

inspector
This commit is contained in:
zhsama
2026-01-27 02:41:57 +08:00
parent 772dbe620d
commit 54fce5e903
16 changed files with 492 additions and 110 deletions

View File

@ -0,0 +1,265 @@
import type { Node } from '@/app/components/workflow/types'
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
import { VarInInspectType } from '@/types/workflow'
const buildAliasVarId = (mapping: AgentAliasMapping) => {
const outputPath = mapping.outputSelector.join('.')
return `alias:${mapping.parentNodeId}:${mapping.extractorNodeId}:${outputPath}`
}
type AgentAliasMapping = {
parentNodeId: string
extractorNodeId: string
outputSelector: string[]
aliasName: string
}
type ToolParameterShape = {
value?: unknown
nested_node_config?: {
extractor_node_id?: string
output_selector?: unknown
}
}
type ToolNodeDataShape = {
tool_parameters?: Record<string, ToolParameterShape>
}
type AliasSource = {
sourceVar: VarInInspect
matchedSelector: string[]
resolvedValue: unknown
resolvedValueFound: boolean
usedFallback: boolean
}
const toSelectorKey = (selector: string[]) => selector.join('.')
const resolveOutputSelector = (extractorNodeId: string, rawSelector?: unknown): string[] => {
if (!Array.isArray(rawSelector))
return []
if (rawSelector[0] === extractorNodeId)
return rawSelector.slice(1)
return rawSelector as string[]
}
const collectAgentAliasMappings = (nodes: Node[]) => {
const nodesById = new Map(nodes.map(node => [node.id, node]))
const mappings: AgentAliasMapping[] = []
const extractorNodeIds = new Set<string>()
nodes.forEach((node) => {
if (node.data.type !== BlockEnum.Tool)
return
const toolData = node.data as ToolNodeDataShape
const toolParams = toolData.tool_parameters || {}
Object.entries(toolParams).forEach(([paramKey, param]) => {
const value = param?.value
if (typeof value !== 'string')
return
const matches = Array.from(value.matchAll(AGENT_CONTEXT_VAR_PATTERN))
if (!matches.length)
return
const agentNodeId = getAgentNodeIdFromContextVar(matches[0][0])
if (!agentNodeId)
return
const extractorNodeId = param?.nested_node_config?.extractor_node_id || `${node.id}_ext_${paramKey}`
extractorNodeIds.add(extractorNodeId)
const resolvedOutputSelector = resolveOutputSelector(extractorNodeId, param?.nested_node_config?.output_selector)
const outputSelector = resolvedOutputSelector.length > 0
? resolvedOutputSelector
: ['structured_output', paramKey]
const agentNode = nodesById.get(agentNodeId)
const aliasName = `@${agentNode?.data.title || agentNodeId}`
mappings.push({
parentNodeId: node.id,
extractorNodeId,
outputSelector,
aliasName,
})
})
})
return { mappings, extractorNodeIds, nodesById }
}
const findAliasSourceVar = (vars: VarInInspect[], extractorNodeId: string, outputSelector: string[]) => {
const selectorKey = toSelectorKey(outputSelector)
return vars.find((varItem) => {
const selector = Array.isArray(varItem.selector) ? varItem.selector : []
if (selector[0] !== extractorNodeId)
return false
return toSelectorKey(selector.slice(1)) === selectorKey
})
}
const resolveNestedValue = (value: unknown, path: string[]): { found: boolean, value: unknown } => {
let current: unknown = value
for (const key of path) {
if (Array.isArray(current)) {
const index = Number.parseInt(key, 10)
if (!Number.isNaN(index) && index >= 0 && index < current.length) {
current = current[index]
continue
}
return { found: false, value: undefined }
}
if (current && typeof current === 'object') {
if (Object.prototype.hasOwnProperty.call(current, key)) {
current = (current as Record<string, unknown>)[key]
continue
}
}
return { found: false, value: undefined }
}
return { found: true, value: current }
}
const resolveAliasSourceVar = (
vars: VarInInspect[],
extractorNodeId: string,
outputSelector: string[],
): AliasSource | undefined => {
const directMatch = findAliasSourceVar(vars, extractorNodeId, outputSelector)
if (directMatch) {
return {
sourceVar: directMatch,
matchedSelector: directMatch.selector as string[],
resolvedValue: directMatch.value,
resolvedValueFound: true,
usedFallback: false,
}
}
if (outputSelector.length === 0)
return undefined
const prefixKey = outputSelector[0]
const prefixVar = vars.find((varItem) => {
const selector = Array.isArray(varItem.selector) ? varItem.selector : []
if (selector[0] !== extractorNodeId)
return false
return selector[1] === prefixKey && selector.length === 2
})
if (!prefixVar)
return undefined
if (outputSelector.length === 1) {
return {
sourceVar: prefixVar,
matchedSelector: prefixVar.selector as string[],
resolvedValue: prefixVar.value,
resolvedValueFound: true,
usedFallback: true,
}
}
const resolved = resolveNestedValue(prefixVar.value, outputSelector.slice(1))
return {
sourceVar: prefixVar,
matchedSelector: prefixVar.selector as string[],
resolvedValue: resolved.value,
resolvedValueFound: resolved.found,
usedFallback: true,
}
}
export const applyAgentSubgraphInspectVars = (nodesWithInspectVars: NodeWithVar[], allNodes: Node[]) => {
const hideExtractorNodes = true
const { mappings, extractorNodeIds, nodesById } = collectAgentAliasMappings(allNodes)
if (mappings.length === 0 && extractorNodeIds.size === 0) {
return nodesWithInspectVars
}
const resultMap = new Map<string, NodeWithVar>()
nodesWithInspectVars.forEach((node) => {
const isExtractorNode = extractorNodeIds.has(node.nodeId)
resultMap.set(node.nodeId, {
...node,
vars: [...node.vars],
isHidden: hideExtractorNodes && isExtractorNode,
})
})
const getOrCreateParentNode = (parentNodeId: string): NodeWithVar | undefined => {
const existing = resultMap.get(parentNodeId)
if (existing)
return existing
const parentNode = nodesById.get(parentNodeId)
if (!parentNode)
return undefined
return {
nodeId: parentNode.id,
nodeType: parentNode.data.type,
title: parentNode.data.title,
nodePayload: parentNode.data,
vars: [] as VarInInspect[],
isValueFetched: false,
isHidden: false,
}
}
mappings.forEach((mapping) => {
const parent = getOrCreateParentNode(mapping.parentNodeId)
if (!parent)
return
const parentVars = parent.vars as VarInInspect[]
const aliasId = buildAliasVarId(mapping)
const upsertAliasVar = (aliasVar: VarInInspect, shouldOverwrite: boolean) => {
const existingIndex = parentVars.findIndex(varItem => varItem.id === aliasVar.id)
if (existingIndex === -1)
parentVars.unshift(aliasVar)
else if (shouldOverwrite)
parentVars[existingIndex] = { ...parentVars[existingIndex], ...aliasVar }
}
const placeholderAliasVar: VarInInspect = {
id: aliasId,
type: VarInInspectType.node,
name: mapping.aliasName,
description: '',
selector: [mapping.parentNodeId, mapping.aliasName],
value_type: VarType.any,
value: undefined,
edited: false,
visible: true,
is_truncated: false,
full_content: { size_bytes: 0, download_url: '' },
aliasMeta: {
extractorNodeId: mapping.extractorNodeId,
outputSelector: mapping.outputSelector,
sourceVarId: aliasId,
},
}
const extractorGroup = nodesWithInspectVars.find(node => node.nodeId === mapping.extractorNodeId)
if (!extractorGroup?.vars?.length) {
upsertAliasVar(placeholderAliasVar, false)
resultMap.set(mapping.parentNodeId, parent)
return
}
const resolved = resolveAliasSourceVar(extractorGroup.vars, mapping.extractorNodeId, mapping.outputSelector)
if (!resolved) {
upsertAliasVar(placeholderAliasVar, false)
resultMap.set(mapping.parentNodeId, parent)
return
}
const resolvedValue = resolved.resolvedValueFound ? resolved.resolvedValue : resolved.sourceVar.value
const aliasVar: VarInInspect = {
...resolved.sourceVar,
id: aliasId,
name: mapping.aliasName,
selector: [mapping.parentNodeId, mapping.aliasName],
value: resolvedValue,
visible: true,
aliasMeta: {
extractorNodeId: mapping.extractorNodeId,
outputSelector: mapping.outputSelector,
sourceVarId: resolved.sourceVar.id,
},
}
upsertAliasVar(aliasVar, true)
resultMap.set(mapping.parentNodeId, parent)
})
// TODO: handle assemble sub-graph output mapping.
return Array.from(resultMap.values())
}

View File

@ -4,6 +4,7 @@ import type { FlowType } from '@/types/common'
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { InteractionMode } from '@/app/components/workflow'
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import {
@ -16,15 +17,18 @@ import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@
import { fetchAllInspectVars } from '@/service/workflow'
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
type Params = {
flowType: FlowType
flowId: string
interactionMode?: InteractionMode
}
export const useSetWorkflowVarsWithValue = ({
flowType,
flowId,
interactionMode,
}: Params) => {
const workflowStore = useWorkflowStore()
const store = useStoreApi()
@ -94,7 +98,10 @@ export const useSetWorkflowVarsWithValue = ({
}
return nodeWithVar
})
setNodesWithInspectVars(res)
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
const shouldApplyAlias = resolvedInteractionMode !== InteractionMode.Subgraph
const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(res, nodeArr) : res
setNodesWithInspectVars(nextNodes)
}
const fetchInspectVars = useCallback(async (params: {

View File

@ -5,6 +5,7 @@ import type { VarInInspect } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { InteractionMode } from '@/app/components/workflow'
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
import {
@ -23,14 +24,17 @@ import {
} from '@/service/use-tools'
import { fetchNodeInspectVars } from '@/service/workflow'
import { VarInInspectType } from '@/types/workflow'
import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
type Params = {
flowId: string
flowType: FlowType
interactionMode?: InteractionMode
}
export const useInspectVarsCrudCommon = ({
flowId,
flowType,
interactionMode,
}: Params) => {
const workflowStore = useWorkflowStore()
const store = useStoreApi()
@ -108,6 +112,7 @@ export const useInspectVarsCrudCommon = ({
const fetchInspectVarValue = useCallback(async (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => {
const {
setNodeInspectVars,
setNodesWithInspectVars,
dataSourceList,
} = workflowStore.getState()
const nodeId = selector[0]
@ -141,7 +146,13 @@ export const useInspectVarsCrudCommon = ({
}
})
setNodeInspectVars(nodeId, varsWithSchemaType)
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools])
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
if (resolvedInteractionMode !== InteractionMode.Subgraph) {
const { nodesWithInspectVars } = workflowStore.getState()
const nextNodes = applyAgentSubgraphInspectVars(nodesWithInspectVars, nodeArr)
setNodesWithInspectVars(nextNodes)
}
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode])
// after last run would call this
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
@ -169,9 +180,12 @@ export const useInspectVarsCrudCommon = ({
}
}
})
setNodesWithInspectVars(nodes)
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
const shouldApplyAlias = resolvedInteractionMode !== InteractionMode.Subgraph
const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(nodes, allNodes) : nodes
setNodesWithInspectVars(nextNodes)
handleCancelNodeSuccessStatus(nodeId)
}, [workflowStore, handleCancelNodeSuccessStatus])
}, [workflowStore, handleCancelNodeSuccessStatus, interactionMode])
const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => {
const { nodesWithInspectVars } = workflowStore.getState()

View File

@ -18,15 +18,7 @@ import { FlowType } from '@/types/common'
// Constants
export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
export const getAgentNodeIdFromContextVar = (placeholder: string): string => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}
export { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
export const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string): string => {
if (!toolNodeId || !paramKey)

View File

@ -15,19 +15,12 @@ import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { BlockEnum } from '@/app/components/workflow/types'
import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context'
import { useGetLanguage } from '@/context/i18n'
import { useStrategyProviders } from '@/service/use-strategy'
import { cn } from '@/utils/classnames'
import { VarType } from './types'
const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
const getAgentNodeIdFromContextVar = (placeholder: string) => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}
type AgentCheckValidContext = {
provider?: StrategyPluginDetail
strategy?: StrategyDetail

View File

@ -0,0 +1,9 @@
export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g
const AGENT_CONTEXT_VAR_PREFIX = '{{@'
const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}'
export const getAgentNodeIdFromContextVar = (placeholder: string): string => {
if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX))
return ''
return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length)
}

View File

@ -11,6 +11,7 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
import { AtSign } from '@/app/components/base/icons/src/vender/workflow'
import Tooltip from '@/app/components/base/tooltip'
import BlockIcon from '@/app/components/workflow/block-icon'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
@ -147,24 +148,32 @@ const Group = ({
{/* var item list */}
{!isCollapsed && !nodeData?.isSingRunRunning && (
<div className="px-0.5">
{visibleVarList.length > 0 && visibleVarList.map(varItem => (
<div
key={varItem.id}
className={cn(
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
)}
onClick={() => handleSelectVar(varItem, varType)}
>
<VariableIconWithColor
variableCategory={varType}
isExceptionVariable={['error_type', 'error_message'].includes(varItem.name)}
className="size-4"
/>
<div className="system-sm-medium grow truncate text-text-secondary">{varItem.name}</div>
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatVarTypeLabel(varItem.value_type)}</div>
</div>
))}
{visibleVarList.length > 0 && visibleVarList.map((varItem) => {
const isAgentAliasVar = typeof varItem.name === 'string' && varItem.name.startsWith('@')
const displayName = isAgentAliasVar ? varItem.name.slice(1) : varItem.name
return (
<div
key={varItem.id}
className={cn(
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
)}
onClick={() => handleSelectVar(varItem, varType)}
>
{isAgentAliasVar
? <AtSign className="size-4 shrink-0 text-util-colors-violet-violet-600" />
: (
<VariableIconWithColor
variableCategory={varType}
isExceptionVariable={['error_type', 'error_message'].includes(varItem.name)}
className="size-4"
/>
)}
<div className="system-sm-medium grow truncate text-text-secondary">{displayName}</div>
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatVarTypeLabel(varItem.value_type)}</div>
</div>
)
})}
</div>
)}
</div>

View File

@ -36,6 +36,8 @@ const Left = ({
} = useCurrentVars()
const { handleNodeSelect } = useNodesInteractions()
const visibleNodesWithInspectVars = nodesWithInspectVars.filter(node => !node.isHidden)
const showDivider = environmentVariables.length > 0 || conversationVars.length > 0 || systemVars.length > 0
const handleClearAll = () => {
@ -91,7 +93,7 @@ const Left = ({
</div>
)}
{/* group nodes */}
{nodesWithInspectVars.length > 0 && nodesWithInspectVars.map(group => (
{visibleNodesWithInspectVars.length > 0 && visibleNodesWithInspectVars.map(group => (
<Group
key={group.nodeId}
varType={VarInInspectType.node}

View File

@ -19,14 +19,15 @@ import Empty from './empty'
import Left from './left'
import Listening from './listening'
import Right from './right'
import { toEnvVarInInspect } from './utils'
export type currentVarType = {
nodeId: string
nodeType: string
title: string
isValueFetched?: boolean
var: VarInInspect
nodeData: NodeProps['data']
var?: VarInInspect
nodeData?: NodeProps['data']
}
const Panel: FC = () => {
@ -59,23 +60,12 @@ const Panel: FC = () => {
return
if (currentFocusNodeId === VarInInspectType.environment) {
const currentVar = environmentVariables.find(v => v.id === currentVarId)
const res = {
return {
nodeId: VarInInspectType.environment,
title: VarInInspectType.environment,
nodeType: VarInInspectType.environment,
var: currentVar ? toEnvVarInInspect(currentVar) : undefined,
}
if (currentVar) {
return {
...res,
var: {
...currentVar,
type: VarInInspectType.environment,
visible: true,
...(currentVar.value_type === 'secret' ? { value: '******************' } : {}),
},
}
}
return res
}
if (currentFocusNodeId === VarInInspectType.conversation) {
const currentVar = conversationVars.find(v => v.id === currentVarId)
@ -83,15 +73,12 @@ const Panel: FC = () => {
nodeId: VarInInspectType.conversation,
title: VarInInspectType.conversation,
nodeType: VarInInspectType.conversation,
}
if (currentVar) {
return {
...res,
var: {
...currentVar,
type: VarInInspectType.conversation,
},
}
var: currentVar
? {
...currentVar,
type: VarInInspectType.conversation,
}
: undefined,
}
return res
}
@ -101,15 +88,12 @@ const Panel: FC = () => {
nodeId: VarInInspectType.system,
title: VarInInspectType.system,
nodeType: VarInInspectType.system,
}
if (currentVar) {
return {
...res,
var: {
...currentVar,
type: VarInInspectType.system,
},
}
var: currentVar
? {
...currentVar,
type: VarInInspectType.system,
}
: undefined,
}
return res
}
@ -124,22 +108,32 @@ const Panel: FC = () => {
isSingRunRunning: targetNode.isSingRunRunning,
isValueFetched: targetNode.isValueFetched,
nodeData: targetNode.nodePayload,
...(currentVar ? { var: currentVar } : {}),
var: currentVar,
}
}, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
const currentAliasMeta = useMemo(() => {
if (!currentFocusNodeId || !currentVarId)
return undefined
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId)
const targetVar = targetNode?.vars.find(v => v.id === currentVarId)
return targetVar?.aliasMeta
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars])
const fetchNodeId = currentAliasMeta?.extractorNodeId || currentFocusNodeId
const isCurrentNodeVarValueFetching = useMemo(() => {
if (!currentNodeInfo)
if (!fetchNodeId)
return false
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentNodeInfo.nodeId)
const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId)
if (!targetNode)
return false
return !targetNode.isValueFetched
}, [currentNodeInfo, nodesWithInspectVars])
}, [fetchNodeId, nodesWithInspectVars])
const handleNodeVarSelect = useCallback((node: currentVarType) => {
setCurrentFocusNodeId(node.nodeId)
setCurrentVarId(node.var.id)
if (node.var)
setCurrentVarId(node.var.id)
}, [setCurrentFocusNodeId, setCurrentVarId])
const { isLoading, schemaTypeDefinitions } = useMatchSchemaType()
@ -150,12 +144,12 @@ const Panel: FC = () => {
}, [eventEmitter])
useEffect(() => {
if (currentFocusNodeId && currentVarId && !isLoading) {
const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId)
if (currentFocusNodeId && currentVarId && !isLoading && fetchNodeId) {
const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId)
if (targetNode && !targetNode.isValueFetched)
fetchInspectVarValue([currentFocusNodeId], schemaTypeDefinitions!)
fetchInspectVarValue([fetchNodeId], schemaTypeDefinitions!)
}
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading])
}, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading, fetchNodeId])
if (isListening) {
return (

View File

@ -55,8 +55,14 @@ const Right = ({
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
const toolIcon = useToolIcon(currentNodeVar?.nodeData)
const isTruncated = currentNodeVar?.var.is_truncated
const fullContent = currentNodeVar?.var.full_content
const currentVar = currentNodeVar?.var
const currentNodeType = currentNodeVar?.nodeType
const currentNodeTitle = currentNodeVar?.title
const currentNodeId = currentNodeVar?.nodeId
const isTruncated = !!currentVar?.is_truncated
const fullContent = currentVar?.full_content
const isAgentAliasVar = currentVar?.name?.startsWith('@')
const displayVarName = isAgentAliasVar ? currentVar?.name?.slice(1) : currentVar?.name
const {
resetConversationVar,
@ -65,15 +71,15 @@ const Right = ({
} = useCurrentVars()
const handleValueChange = (varId: string, value: any) => {
if (!currentNodeVar)
if (!currentNodeVar || !currentVar)
return
editInspectVarValue(currentNodeVar.nodeId, varId, value)
}
const resetValue = () => {
if (!currentNodeVar)
if (!currentNodeVar || !currentVar)
return
resetToLastRunVar(currentNodeVar.nodeId, currentNodeVar.var.id)
resetToLastRunVar(currentNodeVar.nodeId, currentVar.id)
}
const handleClose = () => {
@ -82,13 +88,13 @@ const Right = ({
}
const handleClear = () => {
if (!currentNodeVar)
if (!currentNodeVar || !currentVar)
return
resetConversationVar(currentNodeVar.var.id)
resetConversationVar(currentVar.id)
}
const getCopyContent = () => {
const value = currentNodeVar?.var.value
const value = currentVar?.value
if (value === null || value === undefined)
return ''
@ -160,8 +166,8 @@ const Right = ({
handleHidePromptGenerator()
}, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator])
const schemaType = currentNodeVar?.var.schemaType
const valueType = currentNodeVar?.var.value_type
const schemaType = currentVar?.schemaType
const valueType = currentVar?.value_type
const valueTypeLabel = formatVarTypeLabel(valueType)
const shouldShowSchemaType = !!schemaType
&& schemaType !== valueType
@ -178,32 +184,34 @@ const Right = ({
</ActionButton>
)}
<div className="flex w-0 grow items-center gap-1">
{currentNodeVar?.var && (
{currentVar && (
<>
{
[VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeVar.nodeType as VarInInspectType) && (
currentNodeType
&& [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeType as VarInInspectType) && (
<VariableIconWithColor
variableCategory={currentNodeVar.nodeType as VarInInspectType}
variableCategory={currentNodeType as VarInInspectType}
className="size-4"
/>
)
}
{currentNodeVar.nodeType !== VarInInspectType.environment
&& currentNodeVar.nodeType !== VarInInspectType.conversation
&& currentNodeVar.nodeType !== VarInInspectType.system
{currentNodeType
&& currentNodeType !== VarInInspectType.environment
&& currentNodeType !== VarInInspectType.conversation
&& currentNodeType !== VarInInspectType.system
&& (
<>
<BlockIcon
className="shrink-0"
type={currentNodeVar.nodeType as BlockEnum}
type={currentNodeType as BlockEnum}
size="xs"
toolIcon={toolIcon}
/>
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeVar.title}</div>
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeTitle}</div>
<div className="system-sm-regular shrink-0 text-text-quaternary">/</div>
</>
)}
<div title={currentNodeVar.var.name} className="system-sm-semibold truncate text-text-secondary">{currentNodeVar.var.name}</div>
<div title={displayVarName} className="system-sm-semibold truncate text-text-secondary">{displayVarName}</div>
<div className="system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary">
<span>{`${valueTypeLabel}${displaySchemaType}`}</span>
{isTruncated && (
@ -221,7 +229,7 @@ const Right = ({
)}
</div>
<div className="flex shrink-0 items-center gap-1">
{currentNodeVar && (
{currentVar && (
<>
{canShowPromptGenerator && (
<Tooltip popupContent={t('generate.optimizePromptTooltip', { ns: 'appDebug' })}>
@ -245,27 +253,27 @@ const Right = ({
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentNodeVar.var.edited && (
{!isTruncated && currentVar.edited && (
<Badge>
<span className="ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary"></span>
<span className="system-2xs-semibold-uupercase">{t('debug.variableInspect.edited', { ns: 'workflow' })}</span>
</Badge>
)}
{!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type !== VarInInspectType.conversation && (
{!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.reset', { ns: 'workflow' })}>
<ActionButton onClick={resetValue}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type === VarInInspectType.conversation && (
{!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.resetConversationVar', { ns: 'workflow' })}>
<ActionButton onClick={handleClear}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{currentNodeVar.var.value_type !== 'secret' && (
{currentVar.value_type !== 'secret' && (
<CopyFeedback content={getCopyContent()} />
)}
</>
@ -277,16 +285,16 @@ const Right = ({
</div>
{/* content */}
<div className="grow p-2">
{!currentNodeVar?.var && <Empty />}
{!currentVar && <Empty />}
{isValueFetching && (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)}
{currentNodeVar?.var && !isValueFetching && (
{currentVar && currentNodeId && !isValueFetching && (
<ValueContent
key={`${currentNodeVar.nodeId}-${currentNodeVar.var.id}`}
currentVar={currentNodeVar.var}
key={`${currentNodeId}-${currentVar.id}`}
currentVar={currentVar}
handleValueChange={handleValueChange}
isTruncated={!!isTruncated}
/>

View File

@ -1,4 +1,8 @@
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import type { VarInInspect } from '@/types/workflow'
import { z } from 'zod'
import { VarType } from '@/app/components/workflow/types'
import { VarInInspectType } from '@/types/workflow'
const arrayStringSchemaParttern = z.array(z.string())
const arrayNumberSchemaParttern = z.array(z.number())
@ -10,6 +14,34 @@ type Json = Literal | { [key: string]: Json } | Json[]
const jsonSchema: z.ZodType<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
const arrayJsonSchema: z.ZodType<Json[]> = z.lazy(() => z.array(jsonSchema))
const toEnvVarType = (valueType: EnvironmentVariable['value_type']): VarInInspect['value_type'] => {
switch (valueType) {
case 'number':
return VarType.number
case 'secret':
return VarType.secret
default:
return VarType.string
}
}
export const toEnvVarInInspect = (envVar: EnvironmentVariable): VarInInspect => {
const valueType = envVar.value_type
return {
id: envVar.id,
type: VarInInspectType.environment,
name: envVar.name,
description: envVar.description,
selector: [VarInInspectType.environment, envVar.name],
value_type: toEnvVarType(valueType),
value: valueType === 'secret' ? '******************' : envVar.value,
edited: false,
visible: true,
is_truncated: false,
full_content: { size_bytes: 0, download_url: '' },
}
}
export const validateJSONSchema = (schema: any, type: string) => {
if (type === 'array[string]') {
const result = arrayStringSchemaParttern.safeParse(schema)