refactor: Replace SimpleSelect with PortalToFollowElem in sub-graph

config panel
This commit is contained in:
zhsama
2026-01-22 18:56:53 +08:00
parent 9d80770dfc
commit 73ce9993f2
14 changed files with 183 additions and 82 deletions

View File

@ -328,12 +328,12 @@ const FormInputItem: FC<Props> = ({
&& assemblePlaceholder
&& normalizedValue.includes(assemblePlaceholder)
const resolvedType = isAssembleValue
? VarKindType.mixed
? VarKindType.nested_node
: newType ?? (varInput?.type === VarKindType.nested_node ? VarKindType.nested_node : getVarKindType())
const resolvedNestedNodeConfig = resolvedType === VarKindType.nested_node
? (nestedNodeConfig ?? varInput?.nested_node_config ?? {
extractor_node_id: '',
output_selector: [],
extractor_node_id: nodeId && variable ? `${nodeId}_ext_${variable}` : '',
output_selector: ['result'],
null_strategy: 'use_default',
default_value: '',
})

View File

@ -13,7 +13,7 @@ import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { languages } from '@/i18n-config/language'
import { fetchContextGenerateSuggestedQuestions, generateContext } from '@/service/debug'
import { AppModeEnum } from '@/types/app'
import useContextGenData from '../use-context-gen-data'
import useContextGenData from './use-context-gen-data'
export type ContextGenerateChatMessage = ContextGenerateMessage & {
durationMs?: number
@ -68,6 +68,7 @@ type UseContextGenerateResult = {
handleReset: () => void
handleFetchSuggestedQuestions: () => Promise<void>
abortSuggestedQuestions: () => void
resetSuggestions: () => void
defaultAssistantMessage: string
versionOptions: VersionOption[]
currentVersionLabel: string
@ -98,15 +99,8 @@ const useContextGenerate = ({
{ defaultValue: [] },
)
const [suggestedQuestions, setSuggestedQuestions] = useSessionStorageState<string[]>(
`${storageKey}-suggested-questions`,
{ defaultValue: [] },
)
const [hasFetchedSuggestions, setHasFetchedSuggestions] = useSessionStorageState<boolean>(
`${storageKey}-suggested-questions-fetched`,
{ defaultValue: false },
)
const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([])
const [hasFetchedSuggestions, setHasFetchedSuggestions] = useState<boolean>(false)
const [isFetchingSuggestions, { setTrue: setFetchingSuggestionsTrue, setFalse: setFetchingSuggestionsFalse }] = useBoolean(false)
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
@ -269,8 +263,6 @@ const useContextGenerate = ({
promptLanguage,
setFetchingSuggestionsFalse,
setFetchingSuggestionsTrue,
setHasFetchedSuggestions,
setSuggestedQuestions,
t,
toolNodeId,
])
@ -279,6 +271,11 @@ const useContextGenerate = ({
suggestedQuestionsAbortControllerRef.current?.abort()
}, [])
const resetSuggestions = useCallback(() => {
setSuggestedQuestions([])
setHasFetchedSuggestions(false)
}, [])
const generateStartRef = useRef<number | null>(null)
const handleGenerate = useCallback(async () => {
const trimmed = inputValue.trim()
@ -356,8 +353,8 @@ const useContextGenerate = ({
promptMessages: promptMessages ?? [],
inputValue,
setInputValue,
suggestedQuestions: suggestedQuestions ?? [],
hasFetchedSuggestions: hasFetchedSuggestions ?? false,
suggestedQuestions,
hasFetchedSuggestions,
isGenerating,
model,
handleModelChange,
@ -366,6 +363,7 @@ const useContextGenerate = ({
handleReset,
handleFetchSuggestedQuestions,
abortSuggestedQuestions,
resetSuggestions,
defaultAssistantMessage,
versionOptions,
currentVersionLabel,

View File

@ -104,6 +104,7 @@ const ContextGenerateModal = forwardRef<ContextGenerateModalHandle, Props>(({
handleReset,
handleFetchSuggestedQuestions,
abortSuggestedQuestions,
resetSuggestions,
defaultAssistantMessage,
versionOptions,
currentVersionLabel,
@ -118,8 +119,9 @@ const ContextGenerateModal = forwardRef<ContextGenerateModalHandle, Props>(({
const handleCloseModal = useCallback(() => {
abortSuggestedQuestions()
resetSuggestions()
onClose()
}, [abortSuggestedQuestions, onClose])
}, [abortSuggestedQuestions, onClose, resetSuggestions])
useImperativeHandle(ref, () => ({
onOpen: () => {

View File

@ -0,0 +1,37 @@
// Storage key prefix used by useContextGenData
const CONTEXT_GEN_PREFIX = 'context-gen-'
/**
* Build storage key from flowId, toolNodeId, and paramKey.
* Mirrors the logic in context-generate-modal/index.tsx.
*/
export const buildContextGenStorageKey = (
flowId: string | undefined,
toolNodeId: string,
paramKey: string,
): string => {
const segments = [flowId || 'unknown', toolNodeId, paramKey].filter(Boolean)
return segments.join('-')
}
export const getContextGenStorageKeys = (storageKey: string): string[] => {
return [
`${CONTEXT_GEN_PREFIX}${storageKey}-versions`,
`${CONTEXT_GEN_PREFIX}${storageKey}-version-index`,
`${storageKey}-messages`,
`${storageKey}-suggested-questions`,
`${storageKey}-suggested-questions-fetched`,
]
}
export const clearContextGenStorage = (storageKey: string): void => {
const keys = getContextGenStorageKeys(storageKey)
keys.forEach((key) => {
try {
sessionStorage.removeItem(key)
}
catch {
// Ignore errors (e.g., SSR or private browsing)
}
})
}

View File

@ -1,7 +1,9 @@
export {
AGENT_CONTEXT_VAR_PATTERN,
buildAssembleNestedNodeConfig,
buildAssemblePlaceholder,
getAgentNodeIdFromContextVar,
getDefaultOutputKey,
useMixedVariableExtractor,
} from './use-mixed-variable-extractor'
export type { DetectedAgent } from './use-mixed-variable-extractor'

View File

@ -1,6 +1,7 @@
import type { ReactFlowState } from 'reactflow'
import type { ToolParameter } from '@/app/components/tools/types'
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
import type { NestedNodeConfig } from '@/app/components/workflow/nodes/_base/types'
import type { CodeNodeType, OutputVar } from '@/app/components/workflow/nodes/code/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type {
CommonNodeType,
@ -33,6 +34,31 @@ export const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string)
return `{{#${toolNodeId}_ext_${paramKey}.result#}}`
}
export const getDefaultOutputKey = (outputs?: OutputVar): string => {
if (!outputs)
return ''
const keys = Object.keys(outputs)
if (keys.length === 0)
return ''
// Reason: 'result' is the conventional default output key for code nodes
if (keys.includes('result'))
return 'result'
return keys[0]
}
export const buildAssembleNestedNodeConfig = (
extractorNodeId: string,
outputs?: OutputVar,
): NestedNodeConfig => {
const defaultOutputKey = getDefaultOutputKey(outputs)
return {
extractor_node_id: extractorNodeId,
output_selector: defaultOutputKey ? [defaultOutputKey] : [],
null_strategy: 'use_default',
default_value: '',
}
}
const resolvePromptText = (item?: PromptItem): string => {
if (!item)
return ''

View File

@ -30,10 +30,12 @@ import { useGetLanguage } from '@/context/i18n'
import { useStrategyProviders } from '@/service/use-strategy'
import { cn } from '@/utils/classnames'
import ContextGenerateModal from '../context-generate-modal'
import { buildContextGenStorageKey, clearContextGenStorage } from '../context-generate-modal/utils/storage'
import SubGraphModal from '../sub-graph-modal'
import { AgentHeaderBar, Placeholder } from './components'
import {
AGENT_CONTEXT_VAR_PATTERN,
buildAssembleNestedNodeConfig,
buildAssemblePlaceholder,
getAgentNodeIdFromContextVar,
useMixedVariableExtractor,
@ -319,23 +321,30 @@ const MixedVariableTextInput = ({
return null
const extractorNodeId = assembleExtractorNodeId || `${toolNodeId}_ext_${paramKey}`
ensureAssembleExtractorNode()
onChange?.(assemblePlaceholder, VarKindTypeEnum.mixed, null)
const { getNodes } = reactFlowStore.getState()
const extractorNode = getNodes().find(node => node.id === extractorNodeId)
const outputs = (extractorNode?.data as { outputs?: Record<string, unknown> } | undefined)?.outputs
const nestedNodeConfig = buildAssembleNestedNodeConfig(extractorNodeId, outputs as import('@/app/components/workflow/nodes/code/types').OutputVar | undefined)
onChange?.(assemblePlaceholder, VarKindTypeEnum.nested_node, nestedNodeConfig)
setControlPromptEditorRerenderKey(Date.now())
setIsContextGenerateModalOpen(true)
setTimeout(() => {
contextGenerateModalRef.current?.onOpen()
}, 0)
return [extractorNodeId, 'result']
}, [assembleExtractorNodeId, assemblePlaceholder, ensureAssembleExtractorNode, onChange, paramKey, setControlPromptEditorRerenderKey, toolNodeId])
}, [assembleExtractorNodeId, assemblePlaceholder, ensureAssembleExtractorNode, onChange, paramKey, reactFlowStore, setControlPromptEditorRerenderKey, toolNodeId])
const handleAssembleRemove = useCallback(() => {
if (!onChange || !assemblePlaceholder)
if (!onChange || !assemblePlaceholder || !toolNodeId)
return
removeExtractorNode()
onChange('', VarKindTypeEnum.mixed, null)
setControlPromptEditorRerenderKey(Date.now())
}, [assemblePlaceholder, onChange, removeExtractorNode, setControlPromptEditorRerenderKey])
const storageKey = buildContextGenStorageKey(configsMap?.flowId, toolNodeId, paramKey)
clearContextGenStorage(storageKey)
}, [assemblePlaceholder, configsMap?.flowId, onChange, paramKey, removeExtractorNode, setControlPromptEditorRerenderKey, toolNodeId])
const handleOpenSubGraphModal = useCallback(() => {
setIsSubGraphModalOpen(true)

View File

@ -92,7 +92,9 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
const current = toolParam?.nested_node_config
const rawSelector = Array.isArray(current?.output_selector) ? current!.output_selector : []
const outputSelector = rawSelector[0] === extractorNodeId ? rawSelector.slice(1) : rawSelector
const defaultOutputSelector = ['structured_output', paramKey]
const defaultOutputSelector = isAgentVariant
? ['structured_output', paramKey]
: ['result']
return {
extractor_node_id: current?.extractor_node_id || extractorNodeId,
@ -100,12 +102,9 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
null_strategy: current?.null_strategy || 'use_default',
default_value: current?.default_value ?? '',
}
}, [extractorNodeId, paramKey, toolParam?.nested_node_config])
}, [extractorNodeId, isAgentVariant, paramKey, toolParam?.nested_node_config])
const handleNestedNodeConfigChange = useCallback((config: NestedNodeConfig) => {
if (!isAgentVariant)
return
const { getNodes, setNodes } = reactflowStore.getState()
const nextNodes = getNodes().map((node) => {
if (node.id !== toolNodeId)
@ -124,7 +123,7 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
...toolData.tool_parameters,
[paramKey]: {
...currentParam,
type: currentParam.type || VarKindType.nested_node,
type: VarKindType.nested_node,
nested_node_config: config,
},
},
@ -133,10 +132,10 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
})
setNodes(nextNodes)
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft, isAgentVariant, paramKey, reactflowStore, toolNodeId])
}, [handleSyncWorkflowDraft, paramKey, reactflowStore, toolNodeId])
useEffect(() => {
if (!isAgentVariant || !toolParam || (toolParam.type && toolParam.type !== VarKindType.nested_node))
if (!toolParam || (toolParam.type && toolParam.type !== VarKindType.nested_node))
return
const current = toolParam.nested_node_config
@ -147,7 +146,7 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
if (needsExtractor || needsNullStrategy || needsOutputSelector || needsDefaultValue)
handleNestedNodeConfigChange(nestedNodeConfig)
}, [handleNestedNodeConfigChange, isAgentVariant, nestedNodeConfig, toolParam])
}, [handleNestedNodeConfigChange, nestedNodeConfig, toolParam])
const getUserPromptText = useCallback((promptTemplate?: PromptTemplateItem[] | PromptItem) => {
if (!promptTemplate)
@ -220,6 +219,7 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
if (!toolData.tool_parameters?.[paramKey])
return node
const currentParam = toolData.tool_parameters[paramKey]
return {
...node,
data: {
@ -227,8 +227,10 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
tool_parameters: {
...toolData.tool_parameters,
[paramKey]: {
...toolData.tool_parameters[paramKey],
...currentParam,
type: VarKindType.nested_node,
value: nextValue,
nested_node_config: currentParam.nested_node_config ?? nestedNodeConfig,
},
},
},
@ -238,7 +240,7 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
})
setNodes(nextNodes)
setControlPromptEditorRerenderKey(Date.now())
}, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, toolNodeId])
}, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, nestedNodeConfig, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, toolNodeId])
return (
<Transition appear show={isOpen} as={Fragment}>
@ -298,6 +300,8 @@ const SubGraphModal: FC<SubGraphModalProps> = (props) => {
paramKey={paramKey}
title={props.title}
configsMap={configsMap}
nestedNodeConfig={nestedNodeConfig}
onNestedNodeConfigChange={handleNestedNodeConfigChange}
extractorNode={extractorNode as Node<CodeNodeType> | undefined}
toolParamValue={toolParamValue}
parentAvailableNodes={parentAvailableNodes}