From 0b218adb45caaf251ccc15e2a870ff825300d38a Mon Sep 17 00:00:00 2001
From: Tyler Ohlsen <ohltyler@amazon.com>
Date: Wed, 12 Mar 2025 17:00:19 -0700
Subject: [PATCH] Simplify model i/o when interface defined

Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
---
 .../input_fields/model_field.tsx              |  66 +++---
 .../ml_processor_inputs/model_inputs.tsx      | 197 ++++++++----------
 .../ml_processor_inputs/model_outputs.tsx     | 143 +++++--------
 3 files changed, 168 insertions(+), 238 deletions(-)

diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx
index fc1be031..b8377dbf 100644
--- a/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx
@@ -124,13 +124,15 @@ export function ModelField(props: ModelFieldProps) {
               labelAppend={
                 props.modelCategory ? (
                   <ModelInfoPopover modelCategory={props.modelCategory} />
-                ) : <EuiText size="xs">
-                  <EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank">
-                    Learn more
-                  </EuiLink>
-                </EuiText>
+                ) : (
+                  <EuiText size="xs">
+                    <EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank">
+                      Learn more
+                    </EuiLink>
+                  </EuiText>
+                )
               }
-              helpText={props.helpText || 'The model ID.'}
+              helpText={props.helpText}
               isInvalid={isInvalid}
               error={props.showError && getIn(errors, `${field.name}.id`)}
             >
@@ -142,33 +144,33 @@ export function ModelField(props: ModelFieldProps) {
                     disabled={isEmpty(deployedModels)}
                     options={deployedModels.map(
                       (option) =>
-                      ({
-                        value: option.id,
-                        inputDisplay: (
-                          <>
-                            <EuiText size="s">{option.name}</EuiText>
-                          </>
-                        ),
-                        dropdownDisplay: (
-                          <>
-                            <EuiHealth
-                              color={
-                                isEmpty(option.interface)
-                                  ? 'warning'
-                                  : 'success'
-                              }
-                            >
+                        ({
+                          value: option.id,
+                          inputDisplay: (
+                            <>
                               <EuiText size="s">{option.name}</EuiText>
-                            </EuiHealth>
-                            <EuiText size="xs" color="subdued">
-                              {isEmpty(option.interface)
-                                ? 'Not ready - no model interface'
-                                : 'Deployed'}
-                            </EuiText>
-                          </>
-                        ),
-                        disabled: false,
-                      } as EuiSuperSelectOption<string>)
+                            </>
+                          ),
+                          dropdownDisplay: (
+                            <>
+                              <EuiHealth
+                                color={
+                                  isEmpty(option.interface)
+                                    ? 'warning'
+                                    : 'success'
+                                }
+                              >
+                                <EuiText size="s">{option.name}</EuiText>
+                              </EuiHealth>
+                              <EuiText size="xs" color="subdued">
+                                {isEmpty(option.interface)
+                                  ? 'Not ready - no model interface'
+                                  : 'Deployed'}
+                              </EuiText>
+                            </>
+                          ),
+                          disabled: false,
+                        } as EuiSuperSelectOption<string>)
                     )}
                     valueOfSelected={field.value?.id || ''}
                     onChange={(option: string) => {
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx
index 58b761d0..49a2958d 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx
@@ -35,7 +35,6 @@ import {
   EMPTY_INPUT_MAP_ENTRY,
   WorkflowConfig,
   getCharacterLimitedString,
-  ModelInputFormField,
   INPUT_TRANSFORM_OPTIONS,
 } from '../../../../../../common';
 import {
@@ -47,7 +46,6 @@ import { AppState, getMappings, useAppDispatch } from '../../../../../store';
 import {
   getDataSourceId,
   getObjsFromJSONLines,
-  parseModelInputs,
   sanitizeJSONPath,
 } from '../../../../../utils';
 import { ConfigureExpressionModal, ConfigureTemplateModal } from './modals/';
@@ -108,7 +106,7 @@ export function ModelInputs(props: ModelInputsProps) {
     number | undefined
   >(undefined);
 
-  // on initial load of the models, update model interface states
+  // get the model interface based on the selected ID and list of known models
   useEffect(() => {
     if (!isEmpty(models)) {
       const modelId = getIn(values, modelFieldPath)?.id;
@@ -116,7 +114,7 @@ export function ModelInputs(props: ModelInputsProps) {
         setModelInterface(models[modelId]?.interface);
       }
     }
-  }, [models]);
+  }, [models, getIn(values, modelFieldPath)?.id]);
 
   // persisting doc/query/index mapping fields to collect a list
   // of options to display in the dropdowns when configuring input / output maps
@@ -217,31 +215,6 @@ export function ModelInputs(props: ModelInputsProps) {
     setFieldTouched(inputMapFieldPath, true);
   }
 
-  // The options for keys can change. We update what options are available, based
-  // on if there is a model interface found, and additionally filter out any
-  // options that are already being used in the input map, to discourage duplicate keys.
-  const [keyOptions, setKeyOptions] = useState<ModelInputFormField[]>([]);
-  useEffect(() => {
-    setKeyOptions(parseModelInputs(modelInterface));
-  }, [modelInterface]);
-  useEffect(() => {
-    if (modelInterface !== undefined) {
-      const modelInputs = parseModelInputs(modelInterface);
-      if (getIn(values, inputMapFieldPath) !== undefined) {
-        const existingKeys = getIn(values, inputMapFieldPath).map(
-          (inputMapEntry: InputMapEntry) => inputMapEntry.key
-        ) as string[];
-        setKeyOptions(
-          modelInputs.filter(
-            (modelInput) => !existingKeys.includes(modelInput.label)
-          )
-        );
-      } else {
-        setKeyOptions(modelInputs);
-      }
-    }
-  }, [getIn(values, inputMapFieldPath), modelInterface]);
-
   const valueOptions =
     props.context === PROCESSOR_CONTEXT.INGEST
       ? docFields
@@ -255,37 +228,38 @@ export function ModelInputs(props: ModelInputsProps) {
         const populatedMap = field.value?.length !== 0;
         return (
           <>
-            <EuiPanel>
-              {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && (
-                <>
-                  <BooleanField
-                    fieldPath={oneToOnePath}
-                    label="Merge source data"
-                    type="Switch"
-                    inverse={true}
-                    helpText="Merge multiple documents into a single document for model processing. To process only one document, turn off merge source data."
-                  />
-                  <EuiSpacer size="s" />
-                  {oneToOneChanged && (
-                    <>
-                      <EuiCallOut
-                        size="s"
-                        color="warning"
-                        iconType={'alert'}
-                        title={
-                          <EuiText size="s">
-                            You have changed how source data will be processed.
-                            You may need to update any existing input values to
-                            reflect the updated data structure.
-                          </EuiText>
-                        }
-                      />
-                      <EuiSpacer size="s" />
-                    </>
-                  )}
-                </>
-              )}
-              {populatedMap ? (
+            {populatedMap ? (
+              <EuiPanel>
+                {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && (
+                  <>
+                    <BooleanField
+                      fieldPath={oneToOnePath}
+                      label="Merge source data"
+                      type="Switch"
+                      inverse={true}
+                      helpText="Merge multiple documents into a single document for model processing. To process only one document, turn off merge source data."
+                    />
+                    <EuiSpacer size="s" />
+                    {oneToOneChanged && (
+                      <>
+                        <EuiCallOut
+                          size="s"
+                          color="warning"
+                          iconType={'alert'}
+                          title={
+                            <EuiText size="s">
+                              You have changed how source data will be
+                              processed. You may need to update any existing
+                              input values to reflect the updated data
+                              structure.
+                            </EuiText>
+                          }
+                        />
+                        <EuiSpacer size="s" />
+                      </>
+                    )}
+                  </>
+                )}
                 <EuiCompressedFormRow
                   fullWidth={true}
                   key={inputMapFieldPath}
@@ -343,20 +317,10 @@ export function ModelInputs(props: ModelInputsProps) {
                                     <EuiFlexItem>
                                       <>
                                         {/**
-                                         * We determine if there is an interface based on if there are key options or not,
-                                         * as the options would be derived from the underlying interface.
-                                         * And if so, these values should be static.
-                                         * So, we only display the static text with no mechanism to change it's value.
-                                         * Note we still allow more entries, if a user wants to override / add custom
-                                         * keys if there is some gaps in the model interface.
+                                         * If there is a model interface, display the field name.
+                                         * Otherwise, leave as a free-form text box for a user to enter manually.
                                          */}
-                                        {!isEmpty(keyOptions) &&
-                                        !isEmpty(
-                                          getIn(
-                                            values,
-                                            `${inputMapFieldPath}.${idx}.key`
-                                          )
-                                        ) ? (
+                                        {!isEmpty(modelInterface) ? (
                                           <EuiText
                                             size="s"
                                             style={{ marginTop: '4px' }}
@@ -366,13 +330,6 @@ export function ModelInputs(props: ModelInputsProps) {
                                               `${inputMapFieldPath}.${idx}.key`
                                             )}
                                           </EuiText>
-                                        ) : !isEmpty(keyOptions) ? (
-                                          <SelectWithCustomOptions
-                                            fieldPath={`${inputMapFieldPath}.${idx}.key`}
-                                            options={keyOptions as any[]}
-                                            placeholder={`Name`}
-                                            allowCreate={true}
-                                          />
                                         ) : (
                                           <TextField
                                             fullWidth={true}
@@ -628,16 +585,21 @@ export function ModelInputs(props: ModelInputsProps) {
                                         )}
                                       </>
                                     </EuiFlexItem>
-                                    <EuiFlexItem grow={false}>
-                                      <EuiSmallButtonIcon
-                                        iconType={'trash'}
-                                        color="danger"
-                                        aria-label="Delete"
-                                        onClick={() => {
-                                          deleteMapEntry(field.value, idx);
-                                        }}
-                                      />
-                                    </EuiFlexItem>
+                                    {/**
+                                     * Only allow deleting entries if no defined model interface
+                                     */}
+                                    {isEmpty(modelInterface) && (
+                                      <EuiFlexItem grow={false}>
+                                        <EuiSmallButtonIcon
+                                          iconType={'trash'}
+                                          color="danger"
+                                          aria-label="Delete"
+                                          onClick={() => {
+                                            deleteMapEntry(field.value, idx);
+                                          }}
+                                        />
+                                      </EuiFlexItem>
+                                    )}
                                   </>
                                 </EuiFlexGroup>
                               </EuiFlexItem>
@@ -646,33 +608,38 @@ export function ModelInputs(props: ModelInputsProps) {
                         );
                       }
                     )}
-                    <EuiFlexItem grow={false}>
-                      <div>
-                        <EuiSmallButtonEmpty
-                          style={{ marginLeft: '-8px', marginTop: '0px' }}
-                          iconType={'plusInCircle'}
-                          iconSide="left"
-                          onClick={() => {
-                            addMapEntry(field.value);
-                          }}
-                        >
-                          {`Add input`}
-                        </EuiSmallButtonEmpty>
-                      </div>
-                    </EuiFlexItem>
+                    {/**
+                     * Only allow adding entries if no defined model interface
+                     */}
+                    {isEmpty(modelInterface) && (
+                      <EuiFlexItem grow={false}>
+                        <div>
+                          <EuiSmallButtonEmpty
+                            style={{ marginLeft: '-8px', marginTop: '0px' }}
+                            iconType={'plusInCircle'}
+                            iconSide="left"
+                            onClick={() => {
+                              addMapEntry(field.value);
+                            }}
+                          >
+                            {`Add input`}
+                          </EuiSmallButtonEmpty>
+                        </div>
+                      </EuiFlexItem>
+                    )}
                   </EuiFlexGroup>
                 </EuiCompressedFormRow>
-              ) : (
-                <EuiSmallButton
-                  style={{ width: '100px' }}
-                  onClick={() => {
-                    setFieldValue(field.name, [EMPTY_INPUT_MAP_ENTRY]);
-                  }}
-                >
-                  {'Configure'}
-                </EuiSmallButton>
-              )}
-            </EuiPanel>
+              </EuiPanel>
+            ) : (
+              <EuiSmallButton
+                style={{ width: '100px' }}
+                onClick={() => {
+                  setFieldValue(field.name, [EMPTY_INPUT_MAP_ENTRY]);
+                }}
+              >
+                {'Configure'}
+              </EuiSmallButton>
+            )}
           </>
         );
       }}
diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx
index 2b28c39e..e3d443e3 100644
--- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx
+++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx
@@ -7,6 +7,18 @@ import React, { useState, useEffect } from 'react';
 import { Field, FieldProps, getIn, useFormikContext } from 'formik';
 import { isEmpty } from 'lodash';
 import { useSelector } from 'react-redux';
+import {
+  EuiCompressedFormRow,
+  EuiCompressedSuperSelect,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiPanel,
+  EuiSmallButton,
+  EuiSmallButtonEmpty,
+  EuiSmallButtonIcon,
+  EuiSuperSelectOption,
+  EuiText,
+} from '@elastic/eui';
 import {
   IProcessorConfig,
   IConfigField,
@@ -21,25 +33,10 @@ import {
   OutputMapFormValue,
   EMPTY_OUTPUT_MAP_ENTRY,
   ExpressionVar,
-  ModelOutputFormField,
   OUTPUT_TRANSFORM_OPTIONS,
 } from '../../../../../../common';
-import { SelectWithCustomOptions, TextField } from '../../input_fields';
-
+import { TextField } from '../../input_fields';
 import { AppState } from '../../../../../store';
-import { parseModelOutputs } from '../../../../../utils';
-import {
-  EuiCompressedFormRow,
-  EuiCompressedSuperSelect,
-  EuiFlexGroup,
-  EuiFlexItem,
-  EuiPanel,
-  EuiSmallButton,
-  EuiSmallButtonEmpty,
-  EuiSmallButtonIcon,
-  EuiSuperSelectOption,
-  EuiText,
-} from '@elastic/eui';
 import { ConfigureMultiExpressionModal } from './modals';
 
 interface ModelOutputsProps {
@@ -75,10 +72,6 @@ export function ModelOutputs(props: ModelOutputsProps) {
   const modelFieldPath = `${props.baseConfigPath}.${props.config.id}.${modelField.id}`;
   // Assuming no more than one set of output map entries.
   const outputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.output_map.0`;
-  const fullResponsePath = getIn(
-    values,
-    `${props.baseConfigPath}.${props.config.id}.full_response_path`
-  );
 
   // various modal states
   const [expressionsModalIdx, setExpressionsModalIdx] = useState<
@@ -90,7 +83,7 @@ export function ModelOutputs(props: ModelOutputsProps) {
     ModelInterface | undefined
   >(undefined);
 
-  // on initial load of the models, update model interface states
+  // get the model interface based on the selected ID and list of known models
   useEffect(() => {
     if (!isEmpty(models)) {
       const modelId = getIn(values, modelFieldPath)?.id;
@@ -98,7 +91,7 @@ export function ModelOutputs(props: ModelOutputsProps) {
         setModelInterface(models[modelId]?.interface);
       }
     }
-  }, [models]);
+  }, [models, getIn(values, modelFieldPath)?.id]);
 
   // Adding a map entry to the end of the existing arr
   function addMapEntry(curEntries: OutputMapFormValue): void {
@@ -118,31 +111,6 @@ export function ModelOutputs(props: ModelOutputsProps) {
     setFieldTouched(outputMapFieldPath, true);
   }
 
-  // The options for keys can change. We update what options are available, based
-  // on if there is a model interface found, what full_response_path is, and additionally filter out any
-  // options that are already being used in the output map, to discourage duplicate keys.
-  const [keyOptions, setKeyOptions] = useState<ModelOutputFormField[]>([]);
-  useEffect(() => {
-    setKeyOptions(parseModelOutputs(modelInterface));
-  }, [modelInterface]);
-  useEffect(() => {
-    if (modelInterface !== undefined && fullResponsePath === false) {
-      const modelOutputs = parseModelOutputs(modelInterface);
-      if (getIn(values, outputMapFieldPath) !== undefined) {
-        const existingKeys = getIn(values, outputMapFieldPath).map(
-          (outputMapEntry: OutputMapEntry) => outputMapEntry.key
-        ) as string[];
-        setKeyOptions(
-          modelOutputs.filter(
-            (modelOutput) => !existingKeys.includes(modelOutput.label)
-          )
-        );
-      } else {
-        setKeyOptions(modelOutputs);
-      }
-    }
-  }, [getIn(values, outputMapFieldPath), modelInterface, fullResponsePath]);
-
   return (
     <Field name={outputMapFieldPath} key={outputMapFieldPath}>
       {({ field, form }: FieldProps) => {
@@ -214,20 +182,10 @@ export function ModelOutputs(props: ModelOutputsProps) {
                                     <EuiFlexItem>
                                       <>
                                         {/**
-                                         * We determine if there is an interface based on if there are key options or not,
-                                         * as the options would be derived from the underlying interface.
-                                         * And if so, these values should be static.
-                                         * So, we only display the static text with no mechanism to change it's value.
-                                         * Note we still allow more entries, if a user wants to override / add custom
-                                         * keys if there is some gaps in the model interface.
+                                         * If there is a model interface, display the field name.
+                                         * Otherwise, leave as a free-form text box for a user to enter manually.
                                          */}
-                                        {!isEmpty(keyOptions) &&
-                                        !isEmpty(
-                                          getIn(
-                                            values,
-                                            `${outputMapFieldPath}.${idx}.key`
-                                          )
-                                        ) ? (
+                                        {!isEmpty(modelInterface) ? (
                                           <EuiText
                                             size="s"
                                             style={{ marginTop: '4px' }}
@@ -237,13 +195,6 @@ export function ModelOutputs(props: ModelOutputsProps) {
                                               `${outputMapFieldPath}.${idx}.key`
                                             )}
                                           </EuiText>
-                                        ) : !isEmpty(keyOptions) ? (
-                                          <SelectWithCustomOptions
-                                            fieldPath={`${outputMapFieldPath}.${idx}.key`}
-                                            options={keyOptions as any[]}
-                                            placeholder={`Name`}
-                                            allowCreate={true}
-                                          />
                                         ) : (
                                           <TextField
                                             fullWidth={true}
@@ -438,16 +389,21 @@ export function ModelOutputs(props: ModelOutputsProps) {
                                         ) : undefined}
                                       </>
                                     </EuiFlexItem>
-                                    <EuiFlexItem grow={false}>
-                                      <EuiSmallButtonIcon
-                                        iconType={'trash'}
-                                        color="danger"
-                                        aria-label="Delete"
-                                        onClick={() => {
-                                          deleteMapEntry(field.value, idx);
-                                        }}
-                                      />
-                                    </EuiFlexItem>
+                                    {/**
+                                     * Only allow deleting entries if no defined model interface
+                                     */}
+                                    {isEmpty(modelInterface) && (
+                                      <EuiFlexItem grow={false}>
+                                        <EuiSmallButtonIcon
+                                          iconType={'trash'}
+                                          color="danger"
+                                          aria-label="Delete"
+                                          onClick={() => {
+                                            deleteMapEntry(field.value, idx);
+                                          }}
+                                        />
+                                      </EuiFlexItem>
+                                    )}
                                   </>
                                 </EuiFlexGroup>
                               </EuiFlexItem>
@@ -456,20 +412,25 @@ export function ModelOutputs(props: ModelOutputsProps) {
                         );
                       }
                     )}
-                    <EuiFlexItem grow={false}>
-                      <div>
-                        <EuiSmallButtonEmpty
-                          style={{ marginLeft: '-8px', marginTop: '0px' }}
-                          iconType={'plusInCircle'}
-                          iconSide="left"
-                          onClick={() => {
-                            addMapEntry(field.value);
-                          }}
-                        >
-                          {`Add output`}
-                        </EuiSmallButtonEmpty>
-                      </div>
-                    </EuiFlexItem>
+                    {/**
+                     * Only allow adding entries if no defined model interface
+                     */}
+                    {isEmpty(modelInterface) && (
+                      <EuiFlexItem grow={false}>
+                        <div>
+                          <EuiSmallButtonEmpty
+                            style={{ marginLeft: '-8px', marginTop: '0px' }}
+                            iconType={'plusInCircle'}
+                            iconSide="left"
+                            onClick={() => {
+                              addMapEntry(field.value);
+                            }}
+                          >
+                            {`Add output`}
+                          </EuiSmallButtonEmpty>
+                        </div>
+                      </EuiFlexItem>
+                    )}
                   </EuiFlexGroup>
                 </EuiCompressedFormRow>
               </EuiPanel>