feat: support nested agent output children

This commit is contained in:
yyh 2026-06-23 18:14:34 +08:00
parent c9a3fa9a45
commit 53b59eea8b
No known key found for this signature in database
5 changed files with 652 additions and 70 deletions

View File

@ -0,0 +1,184 @@
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { AgentOutputVariables } from '../index'
const editorLabel = 'workflow.nodes.agent.outputVars.editorLabel'
const nameLabel = 'workflow.nodes.agent.outputVars.nameLabel'
const confirmLabel = 'workflow.nodes.agent.outputVars.confirm'
async function expandOutputVars(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole('button', { name: 'workflow.nodes.common.outputVars' }))
}
function getAddButton(name: string) {
return screen.getByRole('button', { name: `common.operation.add ${name}` })
}
function getEditButton(name: string) {
return screen.getByRole('button', {
name: `workflow.nodes.agent.outputVars.edit:{"name":"${name}"}`,
})
}
async function confirmEditorName(user: ReturnType<typeof userEvent.setup>, name: string) {
const editor = screen.getByRole('form', { name: editorLabel })
const nameInput = within(editor).getByLabelText(nameLabel)
await user.clear(nameInput)
await user.type(nameInput, name)
await user.click(within(editor).getByRole('button', { name: confirmLabel }))
}
describe('AgentOutputVariables', () => {
it('should add an object child without opening the parent editor', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
const outputs: DeclaredOutputConfig[] = [{
name: 'profile',
type: 'object',
required: true,
description: 'User profile',
}]
render(<AgentOutputVariables outputs={outputs} onChange={onChange} />)
await expandOutputVars(user)
await user.click(getAddButton('profile'))
expect(screen.getAllByRole('form', { name: editorLabel })).toHaveLength(1)
expect(screen.getByText('profile')).toBeInTheDocument()
await confirmEditorName(user, 'email')
expect(onChange).toHaveBeenCalledWith([{
name: 'profile',
type: 'object',
required: true,
description: 'User profile',
children: [{
name: 'email',
type: 'string',
required: false,
}],
}])
})
it('should append children to array object items', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
const outputs: DeclaredOutputConfig[] = [{
name: 'addresses',
type: 'array',
required: false,
array_item: {
type: 'object',
},
}]
render(<AgentOutputVariables outputs={outputs} onChange={onChange} />)
await expandOutputVars(user)
await user.click(getAddButton('addresses'))
await confirmEditorName(user, 'city')
expect(onChange).toHaveBeenCalledWith([{
name: 'addresses',
type: 'array',
required: false,
array_item: {
type: 'object',
children: [{
name: 'city',
type: 'string',
required: false,
}],
},
}])
})
it('should add nested children under the selected object child', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
const outputs: DeclaredOutputConfig[] = [{
name: 'profile',
type: 'object',
required: true,
children: [{
name: 'contact',
type: 'object',
required: true,
}],
}]
render(<AgentOutputVariables outputs={outputs} onChange={onChange} />)
await expandOutputVars(user)
await user.click(getAddButton('contact'))
await confirmEditorName(user, 'email')
expect(onChange).toHaveBeenCalledWith([{
name: 'profile',
type: 'object',
required: true,
children: [{
name: 'contact',
type: 'object',
required: true,
children: [{
name: 'email',
type: 'string',
required: false,
}],
}],
}])
})
it('should edit only the selected nested child', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
const outputs: DeclaredOutputConfig[] = [{
name: 'profile',
type: 'object',
required: true,
children: [{
name: 'contact',
type: 'object',
required: true,
children: [{
name: 'email',
type: 'string',
required: true,
description: 'Primary email',
}],
}],
}]
render(<AgentOutputVariables outputs={outputs} onChange={onChange} />)
await expandOutputVars(user)
await user.click(getEditButton('email'))
expect(screen.getAllByRole('form', { name: editorLabel })).toHaveLength(1)
await confirmEditorName(user, 'work_email')
expect(onChange).toHaveBeenCalledWith([{
name: 'profile',
type: 'object',
required: true,
children: [{
name: 'contact',
type: 'object',
required: true,
children: [{
name: 'work_email',
type: 'string',
required: true,
description: 'Primary email',
}],
}],
}])
})
})

View File

@ -6,6 +6,7 @@ import {
} from '../utils'
const createDraft = (overrides: Partial<OutputDraft> = {}): OutputDraft => ({
children: [],
defaultValue: '',
description: '',
name: 'summary',
@ -54,4 +55,49 @@ describe('agent output variables utils', () => {
type: 'array[file]',
}))).toBe('nodes.agent.outputVars.defaultValueFileUnsupported')
})
it('should preserve object children when building an output', () => {
expect(createOutputFromDraft(createDraft({
children: [{
name: 'email',
type: 'string',
required: true,
description: 'User email',
}],
name: 'profile',
type: 'object',
}))).toMatchObject({
name: 'profile',
type: 'object',
children: [{
name: 'email',
type: 'string',
required: true,
description: 'User email',
}],
})
})
it('should preserve array object item children when building an output', () => {
expect(createOutputFromDraft(createDraft({
children: [{
name: 'city',
type: 'string',
required: false,
}],
name: 'addresses',
type: 'array[object]',
}))).toMatchObject({
name: 'addresses',
type: 'array',
array_item: {
type: 'object',
children: [{
name: 'city',
type: 'string',
required: false,
}],
},
})
})
})

View File

@ -1,5 +1,5 @@
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
import type { EditingState, OutputDraft } from './utils'
import type { EditableOutputConfig, EditingState, OutputDraft } from './utils'
import { Button } from '@langgenius/dify-ui/button'
import { CollapsiblePanel, CollapsibleRoot, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import { FieldControl, FieldError, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
@ -38,21 +38,25 @@ function ConfirmHotkeyHint() {
export function OutputEditCard({
existingOutputs,
editingIndex,
allowDefaultValue = true,
state,
onCancel,
onConfirm,
}: {
existingOutputs: DeclaredOutputConfig[]
existingOutputs: EditableOutputConfig[]
editingIndex?: number
allowDefaultValue?: boolean
state: EditingState
onCancel: () => void
onConfirm: (output: DeclaredOutputConfig, index?: number) => void
onConfirm: (output: DeclaredOutputConfig, state: EditingState) => void
}) {
const { t } = useTranslation()
const nameErrorId = useId()
const editorRef = useRef<HTMLDivElement>(null)
const [draft, setDraft] = useState(state.draft)
const trimmedName = draft.name.trim()
const duplicateName = existingOutputs.some((output, index) => output.name === trimmedName && index !== state.index)
const duplicateName = existingOutputs.some((output, index) => output.name === trimmedName && index !== editingIndex)
const nameInvalid = !!trimmedName && !OUTPUT_NAME_PATTERN.test(trimmedName)
const hasNameError = duplicateName || nameInvalid
const defaultValueErrorKey = getDefaultValueErrorKey(draft)
@ -63,7 +67,7 @@ export function OutputEditCard({
function handleConfirm() {
if (confirmDisabled)
return
onConfirm(createOutputFromDraft(draft), state.index)
onConfirm(createOutputFromDraft(draft, { includeDefaultValue: allowDefaultValue }), state)
}
useHotkey(CONFIRM_HOTKEY, handleConfirm, { target: editorRef, ignoreInputs: false })
useHotkey('Escape', onCancel, { target: editorRef, ignoreInputs: false })
@ -134,36 +138,38 @@ export function OutputEditCard({
/>
</FieldRoot>
</div>
<CollapsibleRoot>
<CollapsibleTrigger className="h-8 min-h-8 justify-start gap-x-0.5 rounded-none border-y border-divider-subtle pr-2 pl-2.5 system-xs-regular text-text-tertiary hover:not-data-disabled:bg-state-base-hover hover:not-data-disabled:text-text-tertiary focus-visible:bg-state-base-hover focus-visible:ring-inset data-panel-open:text-text-tertiary">
<span
aria-hidden="true"
className="i-ri-arrow-down-double-line size-3 transition-transform duration-100 ease-out group-data-panel-open:rotate-180 motion-reduce:transition-none"
/>
{t('nodes.agent.outputVars.showAdvancedOptions', { ns: 'workflow' })}
</CollapsibleTrigger>
<CollapsiblePanel className="border-t border-divider-subtle">
<div className="px-3 py-2">
<FieldRoot name="defaultValue" className="gap-1">
<FieldLabel className="py-0 system-xs-medium text-text-secondary">
{t('nodes.agent.outputVars.defaultValueLabel', { ns: 'workflow' })}
</FieldLabel>
<Textarea
size="small"
value={draft.defaultValue}
placeholder={t('nodes.agent.outputVars.defaultValuePlaceholder', { ns: 'workflow' })}
className="mt-1 min-h-6"
onValueChange={defaultValue => updateDraft({ defaultValue })}
/>
{defaultValueErrorKey && (
<FieldError match className="py-0 system-xs-regular text-text-destructive">
{t(defaultValueErrorKey, { ns: 'workflow' })}
</FieldError>
)}
</FieldRoot>
</div>
</CollapsiblePanel>
</CollapsibleRoot>
{allowDefaultValue && (
<CollapsibleRoot>
<CollapsibleTrigger className="h-8 min-h-8 justify-start gap-x-0.5 rounded-none border-y border-divider-subtle pr-2 pl-2.5 system-xs-regular text-text-tertiary hover:not-data-disabled:bg-state-base-hover hover:not-data-disabled:text-text-tertiary focus-visible:bg-state-base-hover focus-visible:ring-inset data-panel-open:text-text-tertiary">
<span
aria-hidden="true"
className="i-ri-arrow-down-double-line size-3 transition-transform duration-100 ease-out group-data-panel-open:rotate-180 motion-reduce:transition-none"
/>
{t('nodes.agent.outputVars.showAdvancedOptions', { ns: 'workflow' })}
</CollapsibleTrigger>
<CollapsiblePanel className="border-t border-divider-subtle">
<div className="px-3 py-2">
<FieldRoot name="defaultValue" className="gap-1">
<FieldLabel className="py-0 system-xs-medium text-text-secondary">
{t('nodes.agent.outputVars.defaultValueLabel', { ns: 'workflow' })}
</FieldLabel>
<Textarea
size="small"
value={draft.defaultValue}
placeholder={t('nodes.agent.outputVars.defaultValuePlaceholder', { ns: 'workflow' })}
className="mt-1 min-h-6"
onValueChange={defaultValue => updateDraft({ defaultValue })}
/>
{defaultValueErrorKey && (
<FieldError match className="py-0 system-xs-regular text-text-destructive">
{t(defaultValueErrorKey, { ns: 'workflow' })}
</FieldError>
)}
</FieldRoot>
</div>
</CollapsiblePanel>
</CollapsibleRoot>
)}
<div className="flex h-12 items-center justify-end gap-x-2 px-3">
<Button type="button" size="small" variant="secondary" onClick={onCancel}>
{t('operation.cancel', { ns: 'common' })}

View File

@ -1,5 +1,6 @@
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
import type { AgentOutputVariablesProps, EditingState } from './utils'
import type { ReactNode } from 'react'
import type { AgentOutputVariablesProps, DeclaredOutputChildConfig, EditableOutputConfig, EditingState } from './utils'
import { Button } from '@langgenius/dify-ui/button'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -7,21 +8,30 @@ import Divider from '@/app/components/base/divider'
import OutputVars from '../../../_base/components/output-vars'
import { OutputEditCard } from './edit-card'
import {
canOutputHaveChildren,
createDraft,
deleteOutputChildAtPath,
getOutputChildren,
getOutputChildrenAtPath,
getOutputDescription,
getOutputDisplayType,
getOutputTypeOptionValue,
insertOutputChildAtPath,
isDefaultOutput,
toDeclaredOutputChild,
updateOutputChildAtPath,
} from './utils'
function OutputRow({
output,
editable,
onAddChild,
onDelete,
onEdit,
}: {
output: DeclaredOutputConfig
output: EditableOutputConfig
editable: boolean
onAddChild?: () => void
onDelete: () => void
onEdit: () => void
}) {
@ -43,6 +53,16 @@ function OutputRow({
</div>
{editable && (
<div className="pointer-events-none flex shrink-0 items-center gap-x-0.5 opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100">
{onAddChild && (
<button
type="button"
aria-label={`${t('operation.add', { ns: 'common' })} ${output.name}`}
className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover-alt hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={onAddChild}
>
<span aria-hidden="true" className="i-ri-add-circle-line size-4" />
</button>
)}
<button
type="button"
aria-label={t('nodes.agent.outputVars.edit', { ns: 'workflow', name: output.name })}
@ -70,6 +90,22 @@ function OutputRow({
</div>
)
}
function ChildOutputFrame({ children, depth }: { children: ReactNode, depth: number }) {
return (
<div className="flex items-stretch">
{Array.from({ length: depth }, (_, index) => (
<div key={index} aria-hidden="true" className="flex w-5 shrink-0 justify-center">
<div className="w-px bg-divider-subtle" />
</div>
))}
<div className="min-w-0 flex-1">
{children}
</div>
</div>
)
}
export function AgentOutputVariables({
outputs,
onChange,
@ -80,49 +116,151 @@ export function AgentOutputVariables({
setEditingState({ draft: createDraft() })
}
function handleEditOutput(index: number) {
setEditingState({ index, draft: createDraft(outputs[index]) })
setEditingState({ outputIndex: index, draft: createDraft(outputs[index]) })
}
function handleNewChild(outputIndex: number, parentPath: number[]) {
setEditingState({ outputIndex, parentPath, draft: createDraft() })
}
function handleEditChild(outputIndex: number, childPath: number[], child: DeclaredOutputChildConfig) {
setEditingState({ outputIndex, childPath, draft: createDraft(child) })
}
function handleDeleteOutput(index: number) {
onChange(outputs.filter((_, outputIndex) => outputIndex !== index))
}
function handleConfirm(output: DeclaredOutputConfig, index?: number) {
if (typeof index === 'number') {
onChange(outputs.map((item, outputIndex) => outputIndex === index ? output : item))
function handleDeleteChild(outputIndex: number, childPath: number[]) {
const parent = outputs[outputIndex]
if (!parent)
return
onChange(outputs.map((item, itemIndex) => (
itemIndex === outputIndex ? deleteOutputChildAtPath(item, childPath) : item
)))
}
function handleConfirm(output: DeclaredOutputConfig, state: EditingState) {
if (typeof state.outputIndex === 'number' && state.parentPath) {
const parent = outputs[state.outputIndex]
if (!parent)
return
const childOutput = toDeclaredOutputChild(output)
onChange(outputs.map((item, outputIndex) => (
outputIndex === state.outputIndex ? insertOutputChildAtPath(item, state.parentPath!, childOutput) : item
)))
}
else if (typeof state.outputIndex === 'number' && state.childPath) {
const childOutput = toDeclaredOutputChild(output)
onChange(outputs.map((item, outputIndex) => (
outputIndex === state.outputIndex ? updateOutputChildAtPath(item, state.childPath!, childOutput) : item
)))
}
else if (typeof state.outputIndex === 'number') {
onChange(outputs.map((item, outputIndex) => outputIndex === state.outputIndex ? output : item))
}
else {
onChange([...outputs, output])
}
setEditingState(null)
}
return (
<OutputVars>
<div className="pb-2">
<div className="flex flex-col">
{outputs.map((output, index) => (
editingState?.index === index
? (
function renderChildren(output: DeclaredOutputConfig, outputIndex: number, depth: number, children: DeclaredOutputChildConfig[], editable: boolean, parentPath: number[] = []) {
return (
<>
{children.map((child, childIndex) => {
const childPath = [...parentPath, childIndex]
const nestedChildren = getOutputChildren(child)
const isEditingChild = editingState?.outputIndex === outputIndex && pathsEqual(editingState.childPath, childPath)
return (
<div key={`${childPath.join('.')}-${child.name}-${getOutputTypeOptionValue(child)}`} className="flex flex-col">
{isEditingChild
? (
<ChildOutputFrame depth={depth}>
<OutputEditCard
allowDefaultValue={false}
editingIndex={childIndex}
existingOutputs={getOutputChildrenAtPath(output, parentPath)}
state={editingState}
onCancel={() => setEditingState(null)}
onConfirm={handleConfirm}
/>
</ChildOutputFrame>
)
: (
<ChildOutputFrame depth={depth}>
<OutputRow
output={child}
editable={editable}
onAddChild={editable && canOutputHaveChildren(child) ? () => handleNewChild(outputIndex, childPath) : undefined}
onDelete={() => handleDeleteChild(outputIndex, childPath)}
onEdit={() => handleEditChild(outputIndex, childPath, child)}
/>
</ChildOutputFrame>
)}
{renderChildren(output, outputIndex, depth + 1, nestedChildren, editable, childPath)}
{editingState?.outputIndex === outputIndex && pathsEqual(editingState.parentPath, childPath) && (
<ChildOutputFrame depth={depth + 1}>
<OutputEditCard
key={`${output.name}-editing`}
existingOutputs={outputs}
allowDefaultValue={false}
existingOutputs={nestedChildren}
state={editingState}
onCancel={() => setEditingState(null)}
onConfirm={handleConfirm}
/>
)
: (
<OutputRow
key={`${output.name}-${getOutputTypeOptionValue(output)}`}
output={output}
editable={!isDefaultOutput(output)}
onDelete={() => handleDeleteOutput(index)}
onEdit={() => handleEditOutput(index)}
/>
)
))}
</ChildOutputFrame>
)}
</div>
)
})}
</>
)
}
return (
<OutputVars>
<div className="pb-2">
<div className="flex flex-col">
{outputs.map((output, index) => {
const editable = !isDefaultOutput(output)
const children = getOutputChildren(output)
const isEditingOutput = editingState?.outputIndex === index && !editingState.childPath && !editingState.parentPath
return (
<div key={`${output.name}-${getOutputTypeOptionValue(output)}`} className="flex flex-col">
{isEditingOutput
? (
<OutputEditCard
key={`${output.name}-editing`}
editingIndex={index}
existingOutputs={outputs}
state={editingState}
onCancel={() => setEditingState(null)}
onConfirm={handleConfirm}
/>
)
: (
<OutputRow
output={output}
editable={editable}
onAddChild={editable && canOutputHaveChildren(output) ? () => handleNewChild(index, []) : undefined}
onDelete={() => handleDeleteOutput(index)}
onEdit={() => handleEditOutput(index)}
/>
)}
{renderChildren(output, index, 1, children, editable)}
{editingState?.outputIndex === index && editingState.parentPath && !editingState.parentPath.length && (
<ChildOutputFrame depth={1}>
<OutputEditCard
allowDefaultValue={false}
existingOutputs={children}
state={editingState}
onCancel={() => setEditingState(null)}
onConfirm={handleConfirm}
/>
</ChildOutputFrame>
)}
</div>
)
})}
<div className="py-1">
<Divider type="horizontal" className="h-px bg-divider-subtle" />
</div>
{editingState && editingState.index == null
{editingState && editingState.outputIndex == null
? (
<OutputEditCard
existingOutputs={outputs}
@ -149,3 +287,10 @@ export function AgentOutputVariables({
</OutputVars>
)
}
function pathsEqual(left?: number[], right?: number[]) {
if (!left || !right || left.length !== right.length)
return false
return left.every((item, index) => item === right[index])
}

View File

@ -2,6 +2,10 @@ import type { DeclaredOutputConfig, DeclaredOutputType } from '@dify/contracts/a
import type { TFunction } from 'i18next'
import { defaultAgentV2DeclaredOutputs } from '../../output-variables'
export type DeclaredOutputChildConfig = NonNullable<DeclaredOutputConfig['children']>[number]
export type EditableOutputConfig = DeclaredOutputConfig | DeclaredOutputChildConfig
export type OutputTypeOptionValue
= DeclaredOutputType
| 'array[boolean]'
@ -23,11 +27,14 @@ export type OutputDraft = {
name: string
required: boolean
type: OutputTypeOptionValue
children: DeclaredOutputChildConfig[]
}
export type EditingState = {
draft: OutputDraft
index?: number
outputIndex?: number
childPath?: number[]
parentPath?: number[]
}
export type AgentOutputVariablesProps = {
@ -51,7 +58,7 @@ export const OUTPUT_TYPE_OPTIONS: OutputTypeOption[] = [
{ value: 'array[file]', label: 'array[file]', type: 'array', arrayItemType: 'file' },
]
export function getOutputTypeOptionValue(output: DeclaredOutputConfig): OutputTypeOptionValue {
export function getOutputTypeOptionValue(output: EditableOutputConfig): OutputTypeOptionValue {
if (output.type !== 'array')
return output.type
@ -62,7 +69,7 @@ export function getOutputTypeOption(value: OutputTypeOptionValue) {
return OUTPUT_TYPE_OPTIONS.find(option => option.value === value) || OUTPUT_TYPE_OPTIONS[0]!
}
export function createDraft(output?: DeclaredOutputConfig): OutputDraft {
export function createDraft(output?: EditableOutputConfig): OutputDraft {
if (!output) {
return {
defaultValue: '',
@ -70,6 +77,7 @@ export function createDraft(output?: DeclaredOutputConfig): OutputDraft {
name: '',
required: false,
type: 'string',
children: [],
}
}
@ -79,10 +87,14 @@ export function createDraft(output?: DeclaredOutputConfig): OutputDraft {
name: output.name,
required: output.required ?? true,
type: getOutputTypeOptionValue(output),
children: getOutputChildren(output),
}
}
export function createOutputFromDraft(draft: OutputDraft): DeclaredOutputConfig {
export function createOutputFromDraft(
draft: OutputDraft,
{ includeDefaultValue = true }: { includeDefaultValue?: boolean } = {},
): DeclaredOutputConfig {
const option = getOutputTypeOption(draft.type)
const output: DeclaredOutputConfig = {
name: draft.name.trim(),
@ -99,6 +111,16 @@ export function createOutputFromDraft(draft: OutputDraft): DeclaredOutputConfig
}
}
if (draft.children.length && draft.type === 'object')
output.children = draft.children
if (draft.children.length && draft.type === 'array[object]') {
output.array_item = {
type: 'object',
children: draft.children,
}
}
if (option.type === 'file') {
output.file = {
extensions: [],
@ -106,7 +128,7 @@ export function createOutputFromDraft(draft: OutputDraft): DeclaredOutputConfig
}
}
if (draft.defaultValue.trim()) {
if (includeDefaultValue && draft.defaultValue.trim()) {
output.failure_strategy = {
on_failure: 'default_value',
default_value: coerceDefaultValue(draft.defaultValue, option),
@ -116,6 +138,113 @@ export function createOutputFromDraft(draft: OutputDraft): DeclaredOutputConfig
return output
}
export function toDeclaredOutputChild(output: DeclaredOutputConfig): DeclaredOutputChildConfig {
return {
name: output.name,
type: output.type,
required: output.required,
...(output.description ? { description: output.description } : {}),
...(output.file ? { file: output.file } : {}),
...(output.children ? { children: output.children } : {}),
...(output.array_item ? { array_item: output.array_item } : {}),
}
}
export function getOutputChildren(output: EditableOutputConfig): DeclaredOutputChildConfig[] {
if (getOutputTypeOptionValue(output) === 'array[object]')
return readOutputChildren(output.array_item?.children)
if (output.type === 'object')
return readOutputChildren(output.children)
return []
}
export function canOutputHaveChildren(output: EditableOutputConfig) {
const type = getOutputTypeOptionValue(output)
return type === 'object' || type === 'array[object]'
}
export function updateOutputChildren(
output: DeclaredOutputConfig,
children: DeclaredOutputChildConfig[],
): DeclaredOutputConfig {
if (getOutputTypeOptionValue(output) === 'array[object]') {
return {
...output,
array_item: {
type: 'object',
...output.array_item,
children: children.length ? children : undefined,
},
}
}
if (output.type === 'object') {
return {
...output,
children: children.length ? children : undefined,
}
}
return output
}
export function getOutputChildrenAtPath(
output: DeclaredOutputConfig,
path: number[],
): DeclaredOutputChildConfig[] {
const target = getOutputChildAtPath(output, path)
return target ? getOutputChildren(target) : getOutputChildren(output)
}
export function getOutputChildAtPath(
output: DeclaredOutputConfig,
path: number[],
): DeclaredOutputChildConfig | undefined {
let current: DeclaredOutputChildConfig | undefined
let children = getOutputChildren(output)
for (const index of path) {
current = children[index]
if (!current)
return undefined
children = getOutputChildren(current)
}
return current
}
export function insertOutputChildAtPath(
output: DeclaredOutputConfig,
parentPath: number[],
child: DeclaredOutputChildConfig,
) {
return updateOutputChildrenAtPath(output, parentPath, children => [...children, child])
}
export function updateOutputChildAtPath(
output: DeclaredOutputConfig,
childPath: number[],
child: DeclaredOutputChildConfig,
) {
const childIndex = childPath.at(-1)
if (childIndex == null)
return output
return updateOutputChildrenAtPath(output, childPath.slice(0, -1), children => children.map((item, index) => index === childIndex ? child : item))
}
export function deleteOutputChildAtPath(
output: DeclaredOutputConfig,
childPath: number[],
) {
const childIndex = childPath.at(-1)
if (childIndex == null)
return output
return updateOutputChildrenAtPath(output, childPath.slice(0, -1), children => children.filter((_, index) => index !== childIndex))
}
export function getDefaultValueErrorKey(draft: OutputDraft) {
const trimmed = draft.defaultValue.trim()
if (!trimmed)
@ -157,7 +286,7 @@ export function isDefaultOutput(output: DeclaredOutputConfig) {
)
}
export function getOutputDescription(output: DeclaredOutputConfig, t: TFunction) {
export function getOutputDescription(output: EditableOutputConfig, t: TFunction) {
if (output.name === 'text')
return t('nodes.agent.outputVars.text', { ns: 'workflow' })
if (output.name === 'files')
@ -167,11 +296,83 @@ export function getOutputDescription(output: DeclaredOutputConfig, t: TFunction)
return output.description || ''
}
export function getOutputDisplayType(output: DeclaredOutputConfig) {
export function getOutputDisplayType(output: EditableOutputConfig) {
return getOutputTypeOption(getOutputTypeOptionValue(output)).label
}
function getOutputDefaultValue(output: DeclaredOutputConfig) {
function readOutputChildren(children: EditableOutputConfig['children']) {
return (children ?? []) as DeclaredOutputChildConfig[]
}
function updateOutputChildrenAtPath(
output: DeclaredOutputConfig,
parentPath: number[],
updater: (children: DeclaredOutputChildConfig[]) => DeclaredOutputChildConfig[],
): DeclaredOutputConfig {
if (!parentPath.length)
return updateOutputChildren(output, updater(getOutputChildren(output)))
const [childIndex, ...restPath] = parentPath
if (childIndex == null)
return output
const children = getOutputChildren(output)
const nextChildren = children.map((child, index) => (
index === childIndex ? updateChildChildrenAtPath(child, restPath, updater) : child
))
return updateOutputChildren(output, nextChildren)
}
function updateChildChildrenAtPath(
child: DeclaredOutputChildConfig,
parentPath: number[],
updater: (children: DeclaredOutputChildConfig[]) => DeclaredOutputChildConfig[],
): DeclaredOutputChildConfig {
if (!parentPath.length)
return updateChildChildren(child, updater(getOutputChildren(child)))
const [childIndex, ...restPath] = parentPath
if (childIndex == null)
return child
const children = getOutputChildren(child)
const nextChildren = children.map((nestedChild, index) => (
index === childIndex ? updateChildChildrenAtPath(nestedChild, restPath, updater) : nestedChild
))
return updateChildChildren(child, nextChildren)
}
function updateChildChildren(
child: DeclaredOutputChildConfig,
children: DeclaredOutputChildConfig[],
): DeclaredOutputChildConfig {
if (getOutputTypeOptionValue(child) === 'array[object]') {
return {
...child,
array_item: {
type: 'object',
...child.array_item,
children: children.length ? children : undefined,
},
}
}
if (child.type === 'object') {
return {
...child,
children: children.length ? children : undefined,
}
}
return child
}
function getOutputDefaultValue(output: EditableOutputConfig) {
if (!('failure_strategy' in output))
return ''
const defaultValue = output.failure_strategy?.default_value
if (defaultValue == null)
return ''