import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen' import type { AgentOutputTypeOptionValue } from './utils' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger, } from '@langgenius/dify-ui/select' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { $getNodeByKey, $getRoot } from 'lexical' import { memo, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { $isAgentOutputBlockNode } from './node' import { AGENT_OUTPUT_NAME_PATTERN, AGENT_OUTPUT_TYPE_OPTIONS, createAgentOutputConfig, getAgentOutputTypeOption, replaceAgentOutputName, } from './utils' type AgentOutputBlockComponentProps = { nodeKey: string name: string outputType: AgentOutputTypeOptionValue isEditing: boolean selectNameOnEdit?: boolean openTypeSelectOnEdit?: boolean outputs: DeclaredOutputConfig[] onChange?: (outputs: DeclaredOutputConfig[], prompt?: string) => void onEdit?: (name: string, outputType: AgentOutputTypeOptionValue) => void } function upsertOutput( outputs: DeclaredOutputConfig[], oldName: string, nextName: string, outputType: AgentOutputTypeOptionValue, ) { const trimmedName = nextName.trim() if (!AGENT_OUTPUT_NAME_PATTERN.test(trimmedName)) return null const existingIndex = outputs.findIndex((output) => output.name === oldName) const duplicateIndex = outputs.findIndex( (output) => output.name === trimmedName && output.name !== oldName, ) if (duplicateIndex >= 0) return null const nextOutput = createAgentOutputConfig(trimmedName, outputType) if (existingIndex >= 0) return outputs.map((output, index) => (index === existingIndex ? nextOutput : output)) return [...outputs, nextOutput] } const AgentOutputBlockComponent = ({ nodeKey, name, outputType, isEditing, selectNameOnEdit = isEditing, openTypeSelectOnEdit = false, outputs, onChange, onEdit, }: AgentOutputBlockComponentProps) => { const { t } = useTranslation() const [editor] = useLexicalComposerContext() const selected = getAgentOutputTypeOption(outputType) const [draftName, setDraftName] = useState(name) const [lastNodeName, setLastNodeName] = useState(name) const [typeSelectOpen, setTypeSelectOpen] = useState(openTypeSelectOnEdit) const nameInputRef = useRef(null) const skipNextBlurCommitRef = useRef(false) const latestDraftNameRef = useRef(name) const skipNameFocusRef = useRef(false) useEffect(() => { if (!isEditing) return if (skipNameFocusRef.current) { skipNameFocusRef.current = false return } const input = nameInputRef.current if (!input) return input.focus() if (selectNameOnEdit) input.setSelectionRange(0, input.value.length) else input.setSelectionRange(input.value.length, input.value.length) }, [isEditing, selectNameOnEdit]) if (name !== lastNodeName) { setLastNodeName(name) setDraftName(name) latestDraftNameRef.current = name } const commitOutput = ( nextName: string, nextType: AgentOutputTypeOptionValue, options: { keepEditing?: boolean openTypeSelectOnEdit?: boolean selectAfterCommit?: boolean } = {}, ) => { const { keepEditing = false, openTypeSelectOnEdit = false, selectAfterCommit = false } = options const trimmedName = nextName.trim() const nextOutputs = upsertOutput(outputs, name, trimmedName, nextType) if (!nextOutputs) { setDraftName(name) return false } let didCommit = false let nextPrompt: string | undefined editor.update(() => { const node = $getNodeByKey(nodeKey) if (!$isAgentOutputBlockNode(node)) return const currentPrompt = $getRoot() .getChildren() .map((node) => node.getTextContent()) .join('\n') node.setOutput( trimmedName, nextType, keepEditing, nextOutputs, onChange, onEdit, false, openTypeSelectOnEdit, ) if (selectAfterCommit) node.selectNext() nextPrompt = replaceAgentOutputName(currentPrompt, name, trimmedName) didCommit = true }) if (!didCommit) return false onChange?.(nextOutputs, nextPrompt) setDraftName(trimmedName) latestDraftNameRef.current = trimmedName return true } const handleTypeSelectOpenChange = (open: boolean) => { setTypeSelectOpen(open) if (!open) { skipNextBlurCommitRef.current = false editor.update(() => { const node = $getNodeByKey(nodeKey) if ($isAgentOutputBlockNode(node)) node.setOpenTypeSelectOnEdit(false) }) } } const handleEditRequest = () => { onEdit?.(name, outputType) } if (!isEditing) { return ( $['nodes.agent.outputVars.edit'], { ns: 'workflow', name })} contentEditable={false} className="group/agent-output inline-flex min-w-[18px] cursor-pointer items-center gap-1 rounded-[5px] border border-util-colors-violet-violet-100 bg-util-colors-violet-violet-50 px-1 py-0.5 align-middle shadow-xs" onClick={handleEditRequest} onKeyDown={(event) => { if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() handleEditRequest() }} >