Просмотр исходного кода

feat(demo): script node (#567)

* feat(demo): script node

* fix: prompt editor with inputs npe
Yiwei Mao 5 месяцев назад
Родитель
Сommit
7e4af6d980
21 измененных файлов с 307 добавлено и 37 удалено
  1. 1 1
      apps/demo-free-layout/src/components/sidebar/sidebar-renderer.tsx
  2. 36 0
      apps/demo-free-layout/src/nodes/code/components/code.tsx
  3. 28 0
      apps/demo-free-layout/src/nodes/code/components/inputs.tsx
  4. 37 0
      apps/demo-free-layout/src/nodes/code/components/outputs.tsx
  5. 33 0
      apps/demo-free-layout/src/nodes/code/form-meta.tsx
  6. 86 0
      apps/demo-free-layout/src/nodes/code/index.tsx
  7. 20 0
      apps/demo-free-layout/src/nodes/code/types.tsx
  8. 1 0
      apps/demo-free-layout/src/nodes/constants.ts
  9. 1 1
      apps/demo-free-layout/src/nodes/http/components/api.tsx
  10. 1 1
      apps/demo-free-layout/src/nodes/http/components/body.tsx
  11. 2 2
      apps/demo-free-layout/src/nodes/http/components/headers.tsx
  12. 2 2
      apps/demo-free-layout/src/nodes/http/components/params.tsx
  13. 2 2
      apps/demo-free-layout/src/nodes/http/components/timeout.tsx
  14. 12 1
      apps/demo-free-layout/src/nodes/http/form-meta.tsx
  15. 3 11
      apps/demo-free-layout/src/nodes/http/index.tsx
  16. 2 0
      apps/demo-free-layout/src/nodes/index.ts
  17. 18 2
      apps/demo-free-layout/src/nodes/loop/form-meta.tsx
  18. 2 11
      apps/demo-free-layout/src/nodes/loop/index.ts
  19. 5 1
      packages/materials/form-materials/src/components/inputs-values/index.tsx
  20. 14 1
      packages/materials/form-materials/src/components/json-schema-editor/index.tsx
  21. 1 1
      packages/materials/form-materials/src/components/prompt-editor-with-inputs/inputs-picker.tsx

+ 1 - 1
apps/demo-free-layout/src/components/sidebar/sidebar-renderer.tsx

@@ -92,7 +92,7 @@ export const SidebarRenderer = () => {
       onCancel={handleClose}
       closable={false}
       motion={false}
-      width={400}
+      width={500}
       headerStyle={{
         display: 'none',
       }}

+ 36 - 0
apps/demo-free-layout/src/nodes/code/components/code.tsx

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/free-layout-editor';
+import { CodeEditor } from '@flowgram.ai/form-materials';
+import { Divider } from '@douyinfe/semi-ui';
+
+import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
+import { FormItem } from '../../../form-components';
+
+export function Code() {
+  const isSidebar = useIsSidebar();
+  const { readonly } = useNodeRenderContext();
+
+  if (!isSidebar) {
+    return null;
+  }
+
+  return (
+    <>
+      <Divider />
+      <Field<string> name="script.content">
+        {({ field }) => (
+          <CodeEditor
+            languageId="typescript"
+            value={field.value}
+            onChange={(value) => field.onChange(value)}
+            readonly={readonly}
+          />
+        )}
+      </Field>
+    </>
+  );
+}

+ 28 - 0
apps/demo-free-layout/src/nodes/code/components/inputs.tsx

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/free-layout-editor';
+import { IFlowValue, InputsValues } from '@flowgram.ai/form-materials';
+
+import { useNodeRenderContext } from '../../../hooks';
+import { FormItem } from '../../../form-components';
+
+export function Inputs() {
+  const { readonly } = useNodeRenderContext();
+
+  return (
+    <FormItem name="inputs" type="object" vertical>
+      <Field<Record<string, IFlowValue | undefined> | undefined> name="inputsValues">
+        {({ field }) => (
+          <InputsValues
+            value={field.value}
+            onChange={(value) => field.onChange(value)}
+            readonly={readonly}
+          />
+        )}
+      </Field>
+    </FormItem>
+  );
+}

+ 37 - 0
apps/demo-free-layout/src/nodes/code/components/outputs.tsx

@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/free-layout-editor';
+import { IJsonSchema, JsonSchemaEditor } from '@flowgram.ai/form-materials';
+import { Divider } from '@douyinfe/semi-ui';
+
+import { useIsSidebar, useNodeRenderContext } from '../../../hooks';
+import { FormItem } from '../../../form-components';
+
+export function Outputs() {
+  const { readonly } = useNodeRenderContext();
+  const isSidebar = useIsSidebar();
+
+  if (!isSidebar) {
+    return null;
+  }
+
+  return (
+    <>
+      <Divider />
+      <FormItem name="outputs" type="object" vertical>
+        <Field<IJsonSchema> name="outputs">
+          {({ field }) => (
+            <JsonSchemaEditor
+              readonly={readonly}
+              value={field.value}
+              onChange={(value) => field.onChange(value)}
+            />
+          )}
+        </Field>
+      </FormItem>
+    </>
+  );
+}

+ 33 - 0
apps/demo-free-layout/src/nodes/code/form-meta.tsx

@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';
+import { createInferInputsPlugin } from '@flowgram.ai/form-materials';
+import { Divider } from '@douyinfe/semi-ui';
+
+import { FormHeader, FormContent, FormOutputs } from '../../form-components';
+import { CodeNodeJSON } from './types';
+import { Outputs } from './components/outputs';
+import { Inputs } from './components/inputs';
+import { Code } from './components/code';
+import { defaultFormMeta } from '../default-form-meta';
+
+export const FormRender = ({ form }: FormRenderProps<CodeNodeJSON>) => (
+  <>
+    <FormHeader />
+    <FormContent>
+      <Inputs />
+      <Code />
+      <Outputs />
+      <FormOutputs />
+    </FormContent>
+  </>
+);
+
+export const formMeta: FormMeta = {
+  render: (props) => <FormRender {...props} />,
+  effect: defaultFormMeta.effect,
+  plugins: [createInferInputsPlugin({ sourceKey: 'inputsValues', targetKey: 'inputs' })],
+};

+ 86 - 0
apps/demo-free-layout/src/nodes/code/index.tsx

@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { nanoid } from 'nanoid';
+
+import { WorkflowNodeType } from '../constants';
+import { FlowNodeRegistry } from '../../typings';
+import iconCode from '../../assets/icon-script.png';
+import { formMeta } from './form-meta';
+
+let index = 0;
+
+const defaultCode = `// Here, you can retrieve input variables from the node using 'params' and output results using 'ret'.
+// 'params' has been correctly injected into the environment.
+// Here's an example of getting the value of the parameter named 'input' from the node input:
+// const input = params.input;
+// Here's an example of outputting a 'ret' object containing multiple data types:
+// const ret = { "name": 'Xiaoming', "hobbies": ["Reading", "Traveling"] };
+
+async function main({ params }) {
+    // Build the output object
+    const ret = {
+        "key0": params.input + params.input, // Concatenate the input parameter 'input' twice
+        "key1": ["hello", "world"], // Output an array
+        "key2": { // Output an Object
+            "key21": "hi"
+        },
+    };
+
+    return ret;
+}`;
+
+export const CodeNodeRegistry: FlowNodeRegistry = {
+  type: WorkflowNodeType.Code,
+  info: {
+    icon: iconCode,
+    description: 'Run the Script',
+  },
+  meta: {
+    size: {
+      width: 360,
+      height: 390,
+    },
+  },
+  onAdd() {
+    return {
+      id: `code_${nanoid(5)}`,
+      type: 'code',
+      data: {
+        title: `Code_${++index}`,
+        inputsValues: {
+          input: { type: 'constant', content: '' },
+        },
+        script: {
+          language: 'javascript',
+          content: defaultCode,
+        },
+        outputs: {
+          type: 'object',
+          properties: {
+            key0: {
+              type: 'string',
+            },
+            key1: {
+              type: 'array',
+              items: {
+                type: 'string',
+              },
+            },
+            key2: {
+              type: 'object',
+              properties: {
+                key21: {
+                  type: 'string',
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+  },
+  formMeta: formMeta,
+};

+ 20 - 0
apps/demo-free-layout/src/nodes/code/types.tsx

@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';
+import { IFlowValue, IJsonSchema } from '@flowgram.ai/form-materials';
+
+export interface CodeNodeJSON extends FlowNodeJSON {
+  data: {
+    title: string;
+    inputsValues: Record<string, IFlowValue>;
+    inputs: IJsonSchema<'object'>;
+    outputs: IJsonSchema<'object'>;
+    script: {
+      language: 'javascript';
+      content: string;
+    };
+  };
+}

+ 1 - 0
apps/demo-free-layout/src/nodes/constants.ts

@@ -8,6 +8,7 @@ export enum WorkflowNodeType {
   End = 'end',
   LLM = 'llm',
   HTTP = 'http',
+  Code = 'code',
   Condition = 'condition',
   Loop = 'loop',
   BlockStart = 'block-start',

+ 1 - 1
apps/demo-free-layout/src/nodes/http/components/api.tsx

@@ -15,7 +15,7 @@ export function Api() {
 
   return (
     <div>
-      <FormItem name="API" required vertical>
+      <FormItem name="API" required vertical type="string">
         <div style={{ display: 'flex', gap: 5 }}>
           <Field<string> name="api.method" defaultValue="GET">
             {({ field }) => (

+ 1 - 1
apps/demo-free-layout/src/nodes/http/components/body.tsx

@@ -74,7 +74,7 @@ export function Body() {
     <Field<string> name="body.bodyType" defaultValue="JSON">
       {({ field }) => (
         <div style={{ marginTop: 5 }}>
-          <FormItem name="Body" vertical>
+          <FormItem name="Body" vertical type="object">
             <Select
               value={field.value}
               onChange={(value) => {

+ 2 - 2
apps/demo-free-layout/src/nodes/http/components/headers.tsx

@@ -4,7 +4,7 @@
  */
 
 import { Field } from '@flowgram.ai/free-layout-editor';
-import { IFlowConstantRefValue, InputsValues } from '@flowgram.ai/form-materials';
+import { IFlowValue, InputsValues } from '@flowgram.ai/form-materials';
 
 import { useNodeRenderContext } from '../../../hooks';
 import { FormItem } from '../../../form-components';
@@ -14,7 +14,7 @@ export function Headers() {
 
   return (
     <FormItem name="headers" type="object" vertical>
-      <Field<Record<string, IFlowConstantRefValue> | undefined> name="headersValues">
+      <Field<Record<string, IFlowValue | undefined> | undefined> name="headersValues">
         {({ field }) => (
           <InputsValues
             value={field.value}

+ 2 - 2
apps/demo-free-layout/src/nodes/http/components/params.tsx

@@ -4,7 +4,7 @@
  */
 
 import { Field } from '@flowgram.ai/free-layout-editor';
-import { IFlowConstantRefValue, InputsValues } from '@flowgram.ai/form-materials';
+import { IFlowValue, InputsValues } from '@flowgram.ai/form-materials';
 
 import { useNodeRenderContext } from '../../../hooks';
 import { FormItem } from '../../../form-components';
@@ -14,7 +14,7 @@ export function Params() {
 
   return (
     <FormItem name="params" type="object" vertical>
-      <Field<Record<string, IFlowConstantRefValue> | undefined> name="paramsValues">
+      <Field<Record<string, IFlowValue | undefined> | undefined> name="paramsValues">
         {({ field }) => (
           <InputsValues
             value={field.value}

+ 2 - 2
apps/demo-free-layout/src/nodes/http/components/timeout.tsx

@@ -14,7 +14,7 @@ export function Timeout() {
 
   return (
     <div>
-      <FormItem name="Timeout(ms)" required style={{ flex: 1 }}>
+      <FormItem name="Timeout(ms)" required style={{ flex: 1 }} type="number">
         <Field<number> name="timeout.timeout" defaultValue={10000}>
           {({ field }) => (
             <InputNumber
@@ -30,7 +30,7 @@ export function Timeout() {
           )}
         </Field>
       </FormItem>
-      <FormItem name="Retry Times" required>
+      <FormItem name="Retry Times" required type="number">
         <Field<number> name="timeout.retryTimes" defaultValue={1}>
           {({ field }) => (
             <InputNumber

+ 12 - 1
apps/demo-free-layout/src/nodes/http/form-render.tsx → apps/demo-free-layout/src/nodes/http/form-meta.tsx

@@ -3,7 +3,8 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { FormRenderProps } from '@flowgram.ai/free-layout-editor';
+import { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';
+import { createInferInputsPlugin } from '@flowgram.ai/form-materials';
 import { Divider } from '@douyinfe/semi-ui';
 
 import { FormHeader, FormContent, FormOutputs } from '../../form-components';
@@ -13,6 +14,7 @@ import { Params } from './components/params';
 import { Headers } from './components/headers';
 import { Body } from './components/body';
 import { Api } from './components/api';
+import { defaultFormMeta } from '../default-form-meta';
 
 export const FormRender = ({ form }: FormRenderProps<HTTPNodeJSON>) => (
   <>
@@ -31,3 +33,12 @@ export const FormRender = ({ form }: FormRenderProps<HTTPNodeJSON>) => (
     </FormContent>
   </>
 );
+
+export const formMeta: FormMeta = {
+  render: (props) => <FormRender {...props} />,
+  effect: defaultFormMeta.effect,
+  plugins: [
+    createInferInputsPlugin({ sourceKey: 'headersValues', targetKey: 'headers' }),
+    createInferInputsPlugin({ sourceKey: 'paramsValues', targetKey: 'params' }),
+  ],
+};

+ 3 - 11
apps/demo-free-layout/src/nodes/http/index.tsx

@@ -4,13 +4,12 @@
  */
 
 import { nanoid } from 'nanoid';
-import { createInferInputsPlugin } from '@flowgram.ai/form-materials';
 
 import { WorkflowNodeType } from '../constants';
 import { FlowNodeRegistry } from '../../typings';
 import iconHTTP from '../../assets/icon-http.svg';
-import { FormRender } from './form-render';
-import { defaultFormMeta } from '../default-form-meta';
+import { formMeta } from './form-meta';
+
 let index = 0;
 
 export const HTTPNodeRegistry: FlowNodeRegistry = {
@@ -50,12 +49,5 @@ export const HTTPNodeRegistry: FlowNodeRegistry = {
       },
     };
   },
-  formMeta: {
-    render: (props) => <FormRender {...props} />,
-    effect: defaultFormMeta.effect,
-    plugins: [
-      createInferInputsPlugin({ sourceKey: 'headersValues', targetKey: 'headers' }),
-      createInferInputsPlugin({ sourceKey: 'paramsValues', targetKey: 'params' }),
-    ],
-  },
+  formMeta: formMeta,
 };

+ 2 - 0
apps/demo-free-layout/src/nodes/index.ts

@@ -12,6 +12,7 @@ import { EndNodeRegistry } from './end';
 import { ContinueNodeRegistry } from './continue';
 import { ConditionNodeRegistry } from './condition';
 import { CommentNodeRegistry } from './comment';
+import { CodeNodeRegistry } from './code';
 import { BreakNodeRegistry } from './break';
 import { BlockStartNodeRegistry } from './block-start';
 import { BlockEndNodeRegistry } from './block-end';
@@ -27,6 +28,7 @@ export const nodeRegistries: FlowNodeRegistry[] = [
   BlockStartNodeRegistry,
   BlockEndNodeRegistry,
   HTTPNodeRegistry,
+  CodeNodeRegistry,
   ContinueNodeRegistry,
   BreakNodeRegistry,
 ];

+ 18 - 2
apps/demo-free-layout/src/nodes/loop/loop-form-render.tsx → apps/demo-free-layout/src/nodes/loop/form-meta.tsx

@@ -3,10 +3,17 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { FormRenderProps, FlowNodeJSON, Field } from '@flowgram.ai/free-layout-editor';
+import { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';
 import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
-import { BatchOutputs, BatchVariableSelector, IFlowRefValue } from '@flowgram.ai/form-materials';
+import {
+  BatchOutputs,
+  BatchVariableSelector,
+  createBatchOutputsFormPlugin,
+  IFlowRefValue,
+  provideBatchInputEffect,
+} from '@flowgram.ai/form-materials';
 
+import { defaultFormMeta } from '../default-form-meta';
 import { useIsSidebar, useNodeRenderContext } from '../../hooks';
 import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
 
@@ -78,3 +85,12 @@ export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
     </>
   );
 };
+
+export const formMeta: FormMeta = {
+  ...defaultFormMeta,
+  render: LoopFormRender,
+  effect: {
+    loopFor: provideBatchInputEffect,
+  },
+  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs' })],
+};

+ 2 - 11
apps/demo-free-layout/src/nodes/loop/index.ts

@@ -9,12 +9,10 @@ import {
   PositionSchema,
   FlowNodeTransformData,
 } from '@flowgram.ai/free-layout-editor';
-import { createBatchOutputsFormPlugin, provideBatchInputEffect } from '@flowgram.ai/form-materials';
 
-import { defaultFormMeta } from '../default-form-meta';
 import { FlowNodeRegistry } from '../../typings';
 import iconLoop from '../../assets/icon-loop.jpg';
-import { LoopFormRender } from './loop-form-render';
+import { formMeta } from './form-meta';
 import { WorkflowNodeType } from '../constants';
 
 let index = 0;
@@ -100,12 +98,5 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
       ],
     };
   },
-  formMeta: {
-    ...defaultFormMeta,
-    render: LoopFormRender,
-    effect: {
-      loopFor: provideBatchInputEffect,
-    },
-    plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs' })],
-  },
+  formMeta,
 };

+ 5 - 1
packages/materials/form-materials/src/components/inputs-values/index.tsx

@@ -14,7 +14,7 @@ import { IFlowConstantRefValue, IFlowValue } from '../../typings';
 import { useObjectList } from '../../hooks';
 import { UIRow, UIRows } from './styles';
 
-export function InputsValues({ value, onChange, style, readonly }: PropsType) {
+export function InputsValues({ value, onChange, style, readonly, constantProps }: PropsType) {
   const { list, updateKey, updateValue, remove, add } = useObjectList<IFlowValue | undefined>({
     value,
     onChange,
@@ -37,6 +37,10 @@ export function InputsValues({ value, onChange, style, readonly }: PropsType) {
               readonly={readonly}
               value={item.value as IFlowConstantRefValue}
               onChange={(v) => updateValue(item.id, v)}
+              constantProps={{
+                ...constantProps,
+                strategies: [...(constantProps?.strategies || [])],
+              }}
             />
             <IconButton
               disabled={readonly}

+ 14 - 1
packages/materials/form-materials/src/components/json-schema-editor/index.tsx

@@ -44,8 +44,9 @@ export function JsonSchemaEditor(props: {
   onChange?: (value: IJsonSchema) => void;
   config?: ConfigType;
   className?: string;
+  readonly?: boolean;
 }) {
-  const { value = { type: 'object' }, config = {}, onChange: onChangeProps } = props;
+  const { value = { type: 'object' }, config = {}, onChange: onChangeProps, readonly } = props;
   const { propertyList, onAddProperty, onRemoveProperty, onEditProperty } = usePropertiesEdit(
     value,
     onChangeProps
@@ -56,6 +57,7 @@ export function JsonSchemaEditor(props: {
       <UIProperties>
         {propertyList.map((_property, index) => (
           <PropertyEdit
+            readonly={readonly}
             key={_property.key}
             value={_property}
             config={config}
@@ -70,6 +72,7 @@ export function JsonSchemaEditor(props: {
         ))}
       </UIProperties>
       <Button
+        disabled={readonly}
         size="small"
         style={{ marginTop: 10, marginLeft: 16 }}
         icon={<IconPlus />}
@@ -86,6 +89,7 @@ function PropertyEdit(props: {
   config?: ConfigType;
   onChange?: (value: PropertyValueType) => void;
   onRemove?: () => void;
+  readonly?: boolean;
   $isLast?: boolean;
   $index?: number;
   $isFirst?: boolean;
@@ -97,6 +101,7 @@ function PropertyEdit(props: {
   const {
     value,
     config,
+    readonly,
     $level = 0,
     onChange: onChangeProps,
     onRemove,
@@ -155,6 +160,7 @@ function PropertyEdit(props: {
           <UIRow>
             <UIName>
               <BlurInput
+                disabled={readonly}
                 placeholder={config?.placeholder ?? 'Input Variable Name'}
                 size="small"
                 value={name}
@@ -164,6 +170,7 @@ function PropertyEdit(props: {
             <UIType>
               <TypeSelector
                 value={typeSelectorValue}
+                readonly={readonly}
                 onChange={(_value) => {
                   onChangeProps?.({
                     ...(value || {}),
@@ -174,12 +181,14 @@ function PropertyEdit(props: {
             </UIType>
             <UIRequired>
               <Checkbox
+                disabled={readonly}
                 checked={isPropertyRequired}
                 onChange={(e) => onChange('isPropertyRequired', e.target.checked)}
               />
             </UIRequired>
             <UIActions>
               <IconButton
+                disabled={readonly}
                 size="small"
                 theme="borderless"
                 icon={expand ? <IconShrink size="small" /> : <IconExpand size="small" />}
@@ -189,6 +198,7 @@ function PropertyEdit(props: {
               />
               {isDrilldownObject && (
                 <IconButton
+                  disabled={readonly}
                   size="small"
                   theme="borderless"
                   icon={<IconAddChildren />}
@@ -199,6 +209,7 @@ function PropertyEdit(props: {
                 />
               )}
               <IconButton
+                disabled={readonly}
                 size="small"
                 theme="borderless"
                 icon={<IconMinus size="small" />}
@@ -210,6 +221,7 @@ function PropertyEdit(props: {
             <UIExpandDetail>
               <UILabel>{config?.descTitle ?? 'Description'}</UILabel>
               <BlurInput
+                disabled={readonly}
                 size="small"
                 value={description}
                 onChange={(value) => onChange('description', value)}
@@ -240,6 +252,7 @@ function PropertyEdit(props: {
             <UIProperties $shrink={true}>
               {propertyList.map((_property, index) => (
                 <PropertyEdit
+                  readonly={readonly}
                   key={_property.key}
                   value={_property}
                   config={config}

+ 1 - 1
packages/materials/form-materials/src/components/prompt-editor-with-inputs/inputs-picker.tsx

@@ -79,7 +79,7 @@ export function InputsPicker({
   const treeData: TreeNodeData[] = useMemo(
     () =>
       Object.entries(inputsValues).map(([key, value]) => {
-        if (value.type === 'ref') {
+        if (value?.type === 'ref') {
           const variable = available.getByKeyPath(value.content || []);
 
           if (variable) {