diff --git a/web/src/pages/agent/canvas/node/card.tsx b/web/src/pages/agent/canvas/node/card.tsx index 19fe5623f..642504714 100644 --- a/web/src/pages/agent/canvas/node/card.tsx +++ b/web/src/pages/agent/canvas/node/card.tsx @@ -61,15 +61,17 @@ export function CardWithForm() { type LabelCardProps = { className?: string; -} & PropsWithChildren; +} & PropsWithChildren & + React.HTMLAttributes; -export function LabelCard({ children, className }: LabelCardProps) { +export function LabelCard({ children, className, ...props }: LabelCardProps) { return (
{children}
diff --git a/web/src/pages/agent/form/components/query-variable.tsx b/web/src/pages/agent/form/components/query-variable.tsx index 327e6ce6b..fec842171 100644 --- a/web/src/pages/agent/form/components/query-variable.tsx +++ b/web/src/pages/agent/form/components/query-variable.tsx @@ -56,6 +56,7 @@ export function QueryVariable({ options={finalOptions} {...field} // allowClear + type={type} > diff --git a/web/src/pages/agent/form/components/select-with-secondary-menu.tsx b/web/src/pages/agent/form/components/select-with-secondary-menu.tsx index d8f336d51..5aff0db4b 100644 --- a/web/src/pages/agent/form/components/select-with-secondary-menu.tsx +++ b/web/src/pages/agent/form/components/select-with-secondary-menu.tsx @@ -23,6 +23,7 @@ import { ChevronDownIcon, XIcon } from 'lucide-react'; import * as React from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { VariableType } from '../../constant'; import { useFilterStructuredOutputByValue, useFindAgentStructuredOutputLabel, @@ -52,6 +53,7 @@ interface GroupedSelectWithSecondaryMenuProps { value?: string; onChange?: (value: string) => void; placeholder?: string; + type?: VariableType; } export function GroupedSelectWithSecondaryMenu({ @@ -59,6 +61,7 @@ export function GroupedSelectWithSecondaryMenu({ value, onChange, placeholder, + type, }: GroupedSelectWithSecondaryMenuProps) { const { t } = useTranslation(); const [open, setOpen] = React.useState(false); @@ -69,6 +72,7 @@ export function GroupedSelectWithSecondaryMenu({ // Find the label of the selected item const flattenedOptions = options.flatMap((g) => g.options); + let selectedItem = flattenedOptions .flatMap((o) => [o, ...(o.children || [])]) .find((o) => o.value === value); @@ -140,7 +144,7 @@ export function GroupedSelectWithSecondaryMenu({ - + {options.map((group, idx) => ( {group.options.map((option) => { @@ -159,6 +163,7 @@ export function GroupedSelectWithSecondaryMenu({ data={option} click={handleSecondaryMenuClick} filteredStructuredOutput={filteredStructuredOutput} + type={type} > ); } diff --git a/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx b/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx index a003bb33a..a75d542d8 100644 --- a/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx +++ b/web/src/pages/agent/form/components/structured-output-secondary-menu.tsx @@ -8,6 +8,7 @@ import { cn } from '@/lib/utils'; import { get, isPlainObject } from 'lodash'; import { ChevronRight } from 'lucide-react'; import { PropsWithChildren, ReactNode, useCallback } from 'react'; +import { VariableType } from '../../constant'; type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode }; @@ -15,12 +16,24 @@ type StructuredOutputSecondaryMenuProps = { data: DataItem; click(option: { label: ReactNode; value: string }): void; filteredStructuredOutput: JSONSchema; + type?: VariableType; } & PropsWithChildren; export function StructuredOutputSecondaryMenu({ data, click, filteredStructuredOutput, + type, }: StructuredOutputSecondaryMenuProps) { + const handleSubMenuClick = useCallback( + (option: { label: ReactNode; value: string }, dataType?: string) => () => { + // The query variable of the iteration operator can only select array type data. + if ((type && type === dataType) || !type) { + click(option); + } + }, + [click, type], + ); + const renderAgentStructuredOutput = useCallback( (values: any, option: { label: ReactNode; value: string }) => { if (isPlainObject(values) && 'properties' in values) { @@ -37,7 +50,7 @@ export function StructuredOutputSecondaryMenu({ return (
  • click(nextOption)} + onClick={handleSubMenuClick(nextOption, dataType)} className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between" > {key} @@ -54,13 +67,16 @@ export function StructuredOutputSecondaryMenu({ return
    ; }, - [click], + [handleSubMenuClick], ); return ( -
  • +
  • click(data)} + className="hover:bg-bg-card py-1 px-2 text-text-primary rounded-sm text-sm flex justify-between items-center" + > {data.label}
  • diff --git a/web/src/pages/agent/utils/filter-agent-structured-output.ts b/web/src/pages/agent/utils/filter-agent-structured-output.ts index 3f51158e9..56e3ec21f 100644 --- a/web/src/pages/agent/utils/filter-agent-structured-output.ts +++ b/web/src/pages/agent/utils/filter-agent-structured-output.ts @@ -2,6 +2,42 @@ import { JSONSchema } from '@/components/jsonjoy-builder'; import { Operator } from '@/constants/agent'; import { isPlainObject } from 'lodash'; +// Loop operators can only accept variables of type list. + +// Recursively traverse the JSON schema, keeping attributes with type "array" and discarding others. + +export function filterLoopOperatorInput( + structuredOutput: JSONSchema, + path = [], +) { + if (typeof structuredOutput === 'boolean') { + return structuredOutput; + } + if ( + structuredOutput.properties && + isPlainObject(structuredOutput.properties) + ) { + const properties = Object.entries({ + ...structuredOutput.properties, + }).reduce( + (pre, [key, value]) => { + if ( + typeof value !== 'boolean' && + (value.type === 'array' || hasArrayChild(value)) + ) { + pre[key] = filterLoopOperatorInput(value, path); + } + return pre; + }, + {} as Record, + ); + + return { ...structuredOutput, properties }; + } + + return structuredOutput; +} + export function filterAgentStructuredOutput( structuredOutput: JSONSchema, operator?: string, @@ -13,32 +49,39 @@ export function filterAgentStructuredOutput( structuredOutput.properties && isPlainObject(structuredOutput.properties) ) { - const filterByPredicate = (predicate: (value: JSONSchema) => boolean) => { - const properties = Object.entries({ - ...structuredOutput.properties, - }).reduce( - (pre, [key, value]) => { - if (predicate(value)) { - pre[key] = value; - } - return pre; - }, - {} as Record, - ); - - return { ...structuredOutput, properties }; - }; - if (operator === Operator.Iteration) { - return filterByPredicate( - (value) => typeof value !== 'boolean' && value.type === 'array', - ); - } else { - return filterByPredicate( - (value) => typeof value !== 'boolean' && value.type !== 'array', - ); + return filterLoopOperatorInput(structuredOutput); } + + return structuredOutput; } return structuredOutput; } + +export function hasArrayChild(data: Record | Array) { + if (Array.isArray(data)) { + for (const value of data) { + if (isPlainObject(value) && value.type === 'array') { + return true; + } + if (hasArrayChild(value)) { + return true; + } + } + } + + if (isPlainObject(data)) { + for (const value of Object.values(data)) { + if (isPlainObject(value) && value.type === 'array') { + return true; + } + + if (hasArrayChild(value)) { + return true; + } + } + } + + return false; +}