trustgraph/ai-context/workbench-ui/docs/tech-specs/flow-configurable-parameters-client.md

32 KiB

Flow Configurable Parameters - Client-Side Technical Specification

Overview

This specification describes the client-side implementation of configurable parameters for flow classes in TrustGraph UI. This complements the server-side implementation by providing dynamic form generation, parameter validation, and user-friendly parameter input interfaces for flow creation.

The client-side implementation enables users to:

  • View available parameters when selecting a flow class
  • Input parameter values through dynamically generated forms
  • Validate parameters according to their schema definitions
  • Launch flows with custom parameter configurations
  • Save and reuse parameter presets for common configurations

Goals

  • Dynamic Form Generation: Automatically create parameter input forms based on flow class parameter schemas
  • Type-Safe Validation: Validate parameter inputs according to schema definitions (string, number, boolean, enum)
  • Intuitive User Experience: Provide clear parameter descriptions, validation feedback, and sensible defaults
  • Integration with Existing UI: Seamlessly integrate with the current CreateDialog and flow management system
  • Parameter Presets: Allow users to save and reuse common parameter configurations
  • Real-time Feedback: Show validation errors and hints as users input parameters
  • Accessibility: Ensure parameter forms are accessible and follow project UI patterns

Architecture

Component Structure

src/components/flows/
├── CreateDialog.tsx              # Enhanced with parameter support
├── ParameterInputs.tsx           # Dynamic parameter form component
├── ParameterPresets.tsx          # Parameter preset management
├── ParameterValidation.tsx       # Validation logic and error display
└── __tests__/
    ├── ParameterInputs.test.tsx
    └── ParameterPresets.test.tsx

src/state/
├── flows.ts                      # Enhanced with parameter support
└── flow-parameters.ts            # Parameter definition fetching and caching

src/model/
└── flow-parameters.ts            # Parameter type definitions and utilities

Data Flow

  1. Parameter Schema Fetching:

    User selects flow class → Fetch parameter definitions → Parse schema → Generate form
    
  2. Parameter Input:

    User inputs values → Validate against schema → Update form state → Enable/disable submit
    
  3. Flow Creation:

    User submits → Validate all parameters → Send to API → Create flow with parameters
    

Technical Design

Core Types

// Parameter schema definition (from server)
interface ParameterSchema {
  type: 'string' | 'number' | 'integer' | 'boolean';
  description?: string;
  default?: any;
  enum?: EnumOption[] | string[]; // Can be rich objects or simple strings
  minimum?: number;
  maximum?: number;
  pattern?: string;
  required?: boolean;
  helper?: string; // Custom helper text
  placeholder?: string; // Custom placeholder text
}

// Rich enum option structure
interface EnumOption {
  id: string; // The actual value
  description: string; // Display text
}

// Flow class structure (from getFlowClass API)
interface FlowClass {
  class: { [processorName: string]: any }; // Processor definitions
  description: string;
  flow: { [stepName: string]: any }; // Flow step definitions
  interfaces: { [interfaceName: string]: any }; // Interface definitions
  parameters?: { [flowParamName: string]: FlowParameterMetadata }; // Maps flow param names to parameter metadata
  tags?: string[];
}

// Flow parameter metadata structure
interface FlowParameterMetadata {
  type: string; // Reference to parameter-type definition name
  description: string; // Human-readable description for UI display
  order: number; // Display order for parameter forms (lower numbers appear first)
}

// Parameter definitions fetched from config
interface ParameterDefinitions {
  [definitionName: string]: ParameterSchema;
}

// User parameter values
interface ParameterValues {
  [flowParamName: string]: any;
}

// Parameter validation result
interface ValidationResult {
  isValid: boolean;
  errors: { [paramName: string]: string };
}

Component Implementation

1. Enhanced CreateDialog

// Key enhancements to existing CreateDialog
const CreateDialog = ({ open, onOpenChange }) => {
  const [flowClass, setFlowClass] = useState<string>();
  const [id, setId] = useState("");
  const [description, setDescription] = useState("");
  const [parameterValues, setParameterValues] = useState<ParameterValues>({});

  // Fetch parameter definitions when flow class is selected
  const {
    parameterDefinitions,
    isLoadingParameters
  } = useFlowParameters(flowClass);

  // Validate form including parameters
  const { isValid, errors } = useParameterValidation(
    flowClass,
    parameterDefinitions,
    parameterValues
  );

  const onSubmit = () => {
    if (!isValid) return;

    flowState.startFlow({
      id,
      flowClass,
      description,
      parameters: parameterValues, // Include parameters
      onSuccess: () => {
        setParameterValues({}); // Clear parameters on success
        // ... rest of success logic
      },
    });
  };

  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      {/* Existing dialog structure */}

      {/* Enhanced with parameter inputs */}
      <ParameterInputs
        flowClass={flowClass}
        parameterDefinitions={parameterDefinitions}
        parameterValues={parameterValues}
        onParameterChange={setParameterValues}
        validationErrors={errors}
      />

      {/* Parameter presets */}
      <ParameterPresets
        flowClass={flowClass}
        onPresetLoad={setParameterValues}
        currentValues={parameterValues}
      />
    </Dialog.Root>
  );
};

2. ParameterInputs Component

Following Project Patterns:

  • Uses common components (TextField, SelectField) instead of raw Chakra
  • Implements Chakra v3 patterns (Field.Root, etc.)
  • Provides proper SelectField descriptions with SelectOptionText
interface ParameterInputsProps {
  flowClass?: string;
  parameterDefinitions: ParameterDefinitions;
  parameterValues: ParameterValues;
  onParameterChange: (values: ParameterValues) => void;
  validationErrors: { [key: string]: string };
}

const ParameterInputs: React.FC<ParameterInputsProps> = ({
  parameterDefinitions,
  parameterValues,
  onParameterChange,
  validationErrors,
}) => {
  const contentRef = useRef<HTMLDivElement>(null);

  const handleParameterChange = (paramName: string, value: any) => {
    onParameterChange({
      ...parameterValues,
      [paramName]: value,
    });
  };

  const renderParameterInput = (paramName: string, schema: ParameterSchema) => {
    const defaultValue = schema.default;
    const value = parameterValues[paramName] ?? defaultValue ?? "";
    const error = validationErrors[paramName];
    const label = (schema.description || paramName) + (schema.required ? " *" : "");

    // Helper text priority: schema.helper -> type-based fallback
    const getHelperText = () => {
      if (schema.helper) return schema.helper;

      switch (schema.type) {
        case 'integer': return 'Enter a whole number';
        case 'number': return 'Enter a number (decimals allowed)';
        case 'boolean': return 'Select true or false';
        case 'string': return schema.enum ? undefined : 'Enter text';
        default: return undefined;
      }
    };

    const helperText = getHelperText();
    const placeholder = schema.placeholder || "";

    // Enum parameters - handle both rich {id, description} and simple string arrays
    if (schema.enum && schema.enum.length > 0) {
      const options = schema.enum.map(option => {
        // Handle both rich {id, description} and simple string enums
        const optionId = typeof option === 'object' ? option.id : option;
        const optionDesc = typeof option === 'object' ? option.description : option;

        return {
          value: optionId,
          label: optionDesc,
          description: (
            <SelectOptionText title={optionDesc}>
              {optionId}
            </SelectOptionText>
          ),
        };
      });

      return (
        <Box key={paramName} mt={5}>
          <SelectField
            label={label}
            items={options}
            value={value ? [value.toString()] : []}
            onValueChange={(values) => {
              const selectedValue = values.length > 0 ? values[0] : "";
              handleParameterChange(paramName, selectedValue);
            }}
            contentRef={contentRef}
          />
          {error && <Text color="red.500" fontSize="sm" mt={1}>{error}</Text>}
          {helperText && (
            <Text fontSize="sm" color="fg.muted" mt={1}>
              {helperText}
            </Text>
          )}
        </Box>
      );
    }

    // Boolean parameters - use Checkbox
    if (schema.type === 'boolean') {
      return (
        <Box key={paramName} mt={5}>
          <Field.Root>
            <Checkbox
              checked={value}
              onChange={(e) => handleParameterChange(paramName, e.target.checked)}
            >
              {label}
            </Checkbox>
            {helperText && <Field.HelperText>{helperText}</Field.HelperText>}
            {error && <Text color="red.500" fontSize="sm" mt={1}>{error}</Text>}
          </Field.Root>
        </Box>
      );
    }

    // Number/Integer parameters - use TextField with type="number"
    if (schema.type === 'number' || schema.type === 'integer') {
      let enhancedHelperText = helperText;
      if (schema.minimum !== undefined || schema.maximum !== undefined) {
        const rangeText = [];
        if (schema.minimum !== undefined) rangeText.push(`min: ${schema.minimum}`);
        if (schema.maximum !== undefined) rangeText.push(`max: ${schema.maximum}`);
        const rangeInfo = rangeText.join(", ");
        enhancedHelperText = enhancedHelperText
          ? `${enhancedHelperText} (${rangeInfo})`
          : rangeInfo;
      }

      return (
        <Box key={paramName} mt={5}>
          <TextField
            label={label}
            helperText={enhancedHelperText}
            placeholder={placeholder}
            value={value.toString()}
            onValueChange={(val) => {
              const numValue = schema.type === 'integer'
                ? parseInt(val, 10)
                : parseFloat(val);
              if (!isNaN(numValue)) {
                handleParameterChange(paramName, numValue);
              } else if (val === "") {
                handleParameterChange(paramName, "");
              }
            }}
            type="number"
            required={schema.required}
          />
          {error && <Text color="red.500" fontSize="sm" mt={1}>{error}</Text>}
        </Box>
      );
    }

    // String parameters - use TextField
    return (
      <Box key={paramName} mt={5}>
        <TextField
          label={label}
          helperText={helperText}
          placeholder={placeholder}
          value={value.toString()}
          onValueChange={(val) => handleParameterChange(paramName, val)}
          required={schema.required}
        />
        {error && <Text color="red.500" fontSize="sm" mt={1}>{error}</Text>}
      </Box>
    );
  };

  return (
    <Box>
      {Object.entries(parameterDefinitions).map(([paramName, schema]) =>
        renderParameterInput(paramName, schema)
      )}
    </Box>
  );
};

3. State Management

Following Project Patterns:

  • Uses TanStack Query for API calls and caching
  • Uses useActivity hook for loading states
  • Uses useNotification hook for error/success messages
// Enhanced flows.ts state
export const useFlows = () => {
  // ... existing code

  /**
   * Enhanced mutation for starting flows with parameters
   */
  const startFlowMutation = useMutation({
    mutationFn: ({ id, flowClass, description, parameters, onSuccess }) => {
      return socket
        .flows()
        .startFlow(id, flowClass, description, parameters)
        .then(() => {
          if (onSuccess) onSuccess();
        });
    },
    onError: (err) => {
      notify.error(err.message);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["flows"] });
      notify.success("Flow started successfully");
    },
  });

  // ... rest of existing code
};

// New flow-parameters.ts state
export const useFlowParameters = (flowClassName?: string) => {
  const socket = useSocket();
  const connectionState = useConnectionState();
  const notify = useNotification();

  const isSocketReady =
    connectionState?.status === "authenticated" ||
    connectionState?.status === "unauthenticated";

  /**
   * Query for fetching parameter definitions for a flow class
   */
  const parametersQuery = useQuery({
    queryKey: ["flow-parameters", flowClassName],
    enabled: isSocketReady && !!flowClassName,
    queryFn: async () => {
      if (!flowClassName) return null;

      // Get flow class definition first
      const flowClass = await socket.flows().getFlowClass(flowClassName);

      // Extract parameter metadata
      const parameterMetadata = flowClass.parameters || {};
      if (Object.keys(parameterMetadata).length === 0) {
        return { parameterDefinitions: {}, parameterMapping: {}, parameterMetadata: {} };
      }

      // Extract unique parameter types for fetching definitions
      const parameterTypes = [...new Set(Object.values(parameterMetadata).map(meta => meta.type))];
      const configKeys = parameterTypes.map(type => ({ type: "parameter-types", key: type }));

      const configResponse = await socket.config().getConfig(configKeys);
      const parameterDefinitions = {};

      // Parse config response to get parameter definitions
      configResponse.values?.forEach(item => {
        if (item.type === "parameter-types") {
          parameterDefinitions[item.key] = JSON.parse(item.value);
        }
      });

      // Create mapping for backwards compatibility
      const parameterMapping = {};
      Object.entries(parameterMetadata).forEach(([paramName, meta]) => {
        parameterMapping[paramName] = meta.type;
      });

      return {
        parameterDefinitions,
        parameterMapping, // Maps flow param names to definition names (backwards compatibility)
        parameterMetadata, // Full metadata with description, order, and type
      };
    },
  });

  useActivity(parametersQuery.isLoading, "Loading flow parameters");

  return {
    parameterDefinitions: parametersQuery.data?.parameterDefinitions || {},
    parameterMapping: parametersQuery.data?.parameterMapping || {},
    parameterMetadata: parametersQuery.data?.parameterMetadata || {},
    isLoading: parametersQuery.isLoading,
    isError: parametersQuery.isError,
    error: parametersQuery.error,
  };
};

4. Parameter Validation

// Custom hook for parameter validation
export const useParameterValidation = (
  flowClass: string,
  parameterDefinitions: ParameterDefinitions,
  parameterValues: ParameterValues
) => {
  return useMemo(() => {
    const errors: { [key: string]: string } = {};
    let isValid = true;

    Object.entries(parameterDefinitions).forEach(([paramName, schema]) => {
      const value = parameterValues[paramName];

      // Check required fields
      if (schema.required && (value === undefined || value === "")) {
        errors[paramName] = `${paramName} is required`;
        isValid = false;
        return;
      }

      // Skip validation for empty optional fields
      if (value === undefined || value === "") {
        return;
      }

      // Type validation
      if (schema.type === 'number' || schema.type === 'integer') {
        const numValue = typeof value === 'string' ? parseFloat(value) : value;
        if (isNaN(numValue)) {
          errors[paramName] = `${paramName} must be a valid number`;
          isValid = false;
          return;
        }

        if (schema.type === 'integer' && !Number.isInteger(numValue)) {
          errors[paramName] = `${paramName} must be an integer`;
          isValid = false;
          return;
        }

        // Range validation
        if (schema.minimum !== undefined && numValue < schema.minimum) {
          errors[paramName] = `${paramName} must be at least ${schema.minimum}`;
          isValid = false;
        }
        if (schema.maximum !== undefined && numValue > schema.maximum) {
          errors[paramName] = `${paramName} must be at most ${schema.maximum}`;
          isValid = false;
        }
      }

      // Enum validation
      if (schema.enum && schema.enum.length > 0) {
        if (!schema.enum.includes(value)) {
          errors[paramName] = `${paramName} must be one of: ${schema.enum.join(', ')}`;
          isValid = false;
        }
      }

      // Pattern validation for strings
      if (schema.pattern && schema.type === 'string') {
        const regex = new RegExp(schema.pattern);
        if (!regex.test(value.toString())) {
          errors[paramName] = `${paramName} format is invalid`;
          isValid = false;
        }
      }
    });

    return { isValid, errors };
  }, [parameterDefinitions, parameterValues]);
};

API Integration

Enhanced Socket API

The socket API has been enhanced to support parameters (already implemented):

// In trustgraph-socket.ts - already updated
startFlow(id: string, class_name: string, description: string, parameters?: { [key: string]: any }) {
  return this.api.makeRequest<FlowRequest, FlowResponse>(
    "flow",
    {
      operation: "start-flow",
      "flow-id": id,
      "class-name": class_name,
      description: description,
      parameters: parameters,
    },
    30000,
  ).then((response) => {
    if (response.error) {
      const errorMessage = typeof response.error === 'object' && response.error.message
        ? response.error.message
        : typeof response.error === 'string'
        ? response.error
        : "Flow start failed";
      throw new Error(errorMessage);
    }
    return response;
  });
}

Config API Integration

// Fetching parameter definitions from config system
const fetchParameterDefinitions = async (definitionNames: string[]) => {
  const configKeys = definitionNames.map(name => ({
    type: "parameter-types",
    key: name
  }));

  const response = await socket.config().getConfig(configKeys);
  const definitions = {};

  response.values?.forEach(item => {
    if (item.type === "parameter-types") {
      definitions[item.key] = JSON.parse(item.value);
    }
  });

  return definitions;
};

Real-World Examples

Flow Class Example

{
  "class": {
    "text-completion:{id}": {
      "model": "{llm-model}",
      "request": "non-persistent://tg/request/text-completion:{id}",
      "response": "non-persistent://tg/response/text-completion:{id}"
    }
  },
  "description": "GraphRAG, DocumentRAG, structured data + knowledge cores",
  "flow": {
    "text-completion:{id}": {
      "model": "{llm-model}",
      "temperature": "{llm-temperature}",
      "request": "non-persistent://tg/request/text-completion:{id}",
      "response": "non-persistent://tg/response/text-completion:{id}"
    }
  },
  "interfaces": { /* ... */ },
  "parameters": {
    "llm-model": {
      "description": "LLM model",
      "order": 1,
      "type": "llm-model"
    },
    "llm-rag-model": {
      "description": "LLM model for RAG",
      "order": 2,
      "type": "llm-model"
    },
    "llm-rag-temperature": {
      "description": "LLM temperature",
      "order": 3,
      "type": "llm-temperature"
    },
    "llm-temperature": {
      "description": "LLM temperature",
      "order": 3,
      "type": "llm-temperature"
    }
  },
  "tags": ["document-rag", "graph-rag"]
}

Parameter Definition Examples

Rich Enum Parameter (LLM Model)

{
  "default": "gemini-2.5-flash-lite",
  "description": "LLM model to use",
  "enum": [
    {
      "description": "Gemini 2.5 Pro",
      "id": "gemini-2.5-pro"
    },
    {
      "description": "Claude 3.5 Sonnet (via VertexAI)",
      "id": "claude-3-5-sonnet@20241022"
    }
  ],
  "required": true,
  "type": "string"
}

String Parameter (Free-form)

{
  "default": "gemini-2.5-flash-lite",
  "description": "LLM model to use",
  "required": true,
  "type": "string",
  "helper": "Enter the model identifier",
  "placeholder": "e.g. gpt-4, claude-3"
}

Number Parameter Example

{
  "default": 0.7,
  "description": "Temperature for model generation",
  "type": "number",
  "minimum": 0.0,
  "maximum": 2.0,
  "required": false,
  "helper": "Controls randomness of model output"
}

Boolean Parameter Example

{
  "default": false,
  "description": "Enable streaming responses",
  "type": "boolean",
  "required": false,
  "helper": "Stream responses as they are generated"
}

Config API Response Example

{
  "values": [
    {
      "type": "parameter-types",
      "key": "llm-model",
      "value": "{\"default\": \"gemini-2.5-flash-lite\", \"description\": \"LLM model to use\", \"enum\": [{\"description\": \"Gemini 2.5 Pro\", \"id\": \"gemini-2.5-pro\"}], \"required\": true, \"type\": \"string\"}"
    }
  ]
}

User Experience Design

Form Behavior

  1. Initial State: When CreateDialog opens, no parameters are shown
  2. Flow Class Selection: When user selects a flow class:
    • Show loading indicator while fetching parameters
    • Display parameter form sections if parameters exist
    • Show "No additional parameters required" if none exist
  3. Parameter Input:
    • Show validation errors in real-time
    • Disable submit button until all required parameters are valid
    • Provide clear descriptions and hints for each parameter
  4. Form Submission:
    • Validate all parameters before submission
    • Show progress indicator during submission
    • Clear form on successful submission
    • Retain values on error for correction

Parameter Input Types

  1. Enum Parameters (with rich options):

    • SelectField dropdown with {id, description} structure
    • Display user-friendly descriptions, store technical IDs
    • Example: "Gemini 2.5 Pro" displays, "gemini-2.5-pro" is the value
    • Supports both rich objects and simple string arrays
  2. String Parameters (no enum):

    • TextField for free-form text input
    • Uses description field as label
    • Uses helper field or falls back to "Enter text"
    • Uses placeholder field if provided
  3. Integer Parameters:

    • TextField with type="number"
    • Helper text: "Enter a whole number"
    • Validates and converts to integer
  4. Number/Float Parameters:

    • TextField with type="number"
    • Helper text: "Enter a number (decimals allowed)"
    • Validates and converts to float
  5. Boolean Parameters:

    • Checkbox component
    • Helper text: "Select true or false"
    • Direct true/false values
  6. Required Parameters:

    • Marked with asterisk (*) in label
    • Red error styling for validation failures
    • Cannot submit form without valid values
  7. Default Values:

    • Pre-populated when form loads
    • Shows current value or default from schema

Parameter Presets (Future Enhancement)

// Parameter preset management component
const ParameterPresets: React.FC<{
  flowClass: string;
  onPresetLoad: (values: ParameterValues) => void;
  currentValues: ParameterValues;
}> = ({ flowClass, onPresetLoad, currentValues }) => {
  const [presets, setPresets] = useState<ParameterPreset[]>([]);
  const [presetName, setPresetName] = useState("");

  const savePreset = () => {
    const preset: ParameterPreset = {
      id: generateId(),
      name: presetName,
      flowClass,
      values: currentValues,
      createdAt: new Date(),
    };
    // Save to local storage or backend
    saveParameterPreset(preset);
    setPresets([...presets, preset]);
  };

  return (
    <Box mt={5}>
      <Text fontWeight="bold" mb={2}>Parameter Presets</Text>

      {/* Preset selection */}
      {presets.length > 0 && (
        <SelectField
          label="Load Preset"
          items={presets.map(preset => ({
            value: preset.id,
            label: preset.name,
            description: (
              <SelectOptionText title={preset.name}>
                {preset.name} - {formatDate(preset.createdAt)}
              </SelectOptionText>
            ),
          }))}
          value={[]}
          onValueChange={(values) => {
            const presetId = values[0];
            const preset = presets.find(p => p.id === presetId);
            if (preset) {
              onPresetLoad(preset.values);
            }
          }}
        />
      )}

      {/* Save current values as preset */}
      <HStack mt={3}>
        <TextField
          label="Preset Name"
          value={presetName}
          onValueChange={setPresetName}
          placeholder="Enter preset name..."
        />
        <Button
          onClick={savePreset}
          disabled={!presetName.trim() || Object.keys(currentValues).length === 0}
        >
          Save Preset
        </Button>
      </HStack>
    </Box>
  );
};

Testing Strategy

Unit Tests

  1. ParameterInputs Component Tests:
describe('ParameterInputs', () => {
  it('renders string parameters with TextField', () => {
    const schema = { type: 'string', description: 'Test string param' };
    render(<ParameterInputs parameterDefinitions={{ param1: schema }} ... />);
    expect(screen.getByRole('textbox')).toBeInTheDocument();
  });

  it('renders enum parameters with SelectField', () => {
    const schema = { type: 'string', enum: ['option1', 'option2'] };
    render(<ParameterInputs parameterDefinitions={{ param1: schema }} ... />);
    expect(screen.getByRole('combobox')).toBeInTheDocument();
  });

  it('validates required parameters', () => {
    const schema = { type: 'string', required: true };
    const validationErrors = { param1: 'param1 is required' };
    render(<ParameterInputs validationErrors={validationErrors} ... />);
    expect(screen.getByText('param1 is required')).toBeInTheDocument();
  });

  it('calls onParameterChange when value changes', () => {
    const mockOnChange = jest.fn();
    const schema = { type: 'string' };
    render(<ParameterInputs onParameterChange={mockOnChange} ... />);

    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: 'test value' } });

    expect(mockOnChange).toHaveBeenCalledWith({ param1: 'test value' });
  });
});
  1. Parameter Validation Tests:
describe('useParameterValidation', () => {
  it('validates required parameters', () => {
    const { result } = renderHook(() => useParameterValidation(
      'test-flow',
      { param1: { type: 'string', required: true } },
      {}
    ));

    expect(result.current.isValid).toBe(false);
    expect(result.current.errors.param1).toBe('param1 is required');
  });

  it('validates number ranges', () => {
    const { result } = renderHook(() => useParameterValidation(
      'test-flow',
      { param1: { type: 'number', minimum: 5, maximum: 10 } },
      { param1: 15 }
    ));

    expect(result.current.isValid).toBe(false);
    expect(result.current.errors.param1).toContain('must be at most 10');
  });

  it('validates enum values', () => {
    const { result } = renderHook(() => useParameterValidation(
      'test-flow',
      { param1: { type: 'string', enum: ['opt1', 'opt2'] } },
      { param1: 'invalid' }
    ));

    expect(result.current.isValid).toBe(false);
    expect(result.current.errors.param1).toContain('must be one of: opt1, opt2');
  });
});

Integration Tests

  1. CreateDialog with Parameters:
describe('CreateDialog with Parameters', () => {
  it('fetches and displays parameters when flow class is selected', async () => {
    const mockFlowClass = {
      parameters: { 'model': 'llm-model', 'temp': 'temperature' }
    };
    const mockParameterDefs = {
      'llm-model': { type: 'string', enum: ['gpt-4', 'claude-3'] },
      'temperature': { type: 'number', minimum: 0, maximum: 2 }
    };

    mockSocket.flows().getFlowClass.mockResolvedValue(mockFlowClass);
    mockSocket.config().getConfig.mockResolvedValue({
      values: [
        { type: 'parameters', key: 'llm-model', value: JSON.stringify(mockParameterDefs['llm-model']) },
        { type: 'parameters', key: 'temperature', value: JSON.stringify(mockParameterDefs.temperature) }
      ]
    });

    render(<CreateDialog open={true} />);

    // Select flow class
    const flowClassSelect = screen.getByLabelText('Flow class');
    fireEvent.change(flowClassSelect, { target: { value: 'test-flow' } });

    // Wait for parameters to load and display
    await waitFor(() => {
      expect(screen.getByLabelText('model')).toBeInTheDocument();
      expect(screen.getByLabelText('temp')).toBeInTheDocument();
    });
  });

  it('submits flow with parameter values', async () => {
    // Set up mocks and render
    // Fill in form including parameters
    // Submit form
    // Verify startFlow called with correct parameters
  });

  it('prevents submission with invalid parameters', () => {
    // Set up form with required parameters
    // Leave parameters empty
    // Verify submit button is disabled
    // Verify validation errors are shown
  });
});

End-to-End Tests

  1. Complete Flow Creation with Parameters:

    • Navigate to Flows page
    • Click Create button
    • Select flow class with parameters
    • Fill in all required fields and parameters
    • Submit form
    • Verify flow appears in list
    • Verify flow has correct parameter values
  2. Parameter Validation Scenarios:

    • Test all parameter types (string, number, boolean, enum)
    • Test required field validation
    • Test range validation for numbers
    • Test enum value validation
    • Test form reset after successful submission

Migration Plan

Phase 1: Core Parameter Support

  1. Update FlowRequest/FlowResponse types (completed)
  2. Update startFlow API method (completed)
  3. Create ParameterInputs component (completed)
  4. Integrate ParameterInputs into CreateDialog
  5. Implement parameter fetching from config API
  6. Add parameter validation logic

Phase 2: Enhanced User Experience

  1. Add loading states during parameter fetching
  2. Improve validation error display
  3. Add parameter descriptions and help text
  4. Implement form reset on successful submission

Phase 3: Advanced Features (Future)

  1. Parameter presets and templates
  2. Parameter value suggestions based on history
  3. Bulk parameter import/export
  4. Parameter dependency validation
  5. Real-time parameter preview

Security Considerations

  1. Parameter Validation: All parameter validation occurs on both client and server
  2. Type Safety: TypeScript ensures type safety for parameter values
  3. Sanitization: Parameter values are properly sanitized before API calls
  4. Schema Validation: Parameter schemas are validated against expected formats
  5. Error Handling: Sensitive information is not exposed in validation errors

Performance Considerations

  1. Lazy Loading: Parameter definitions are only fetched when needed
  2. Caching: Parameter definitions are cached using TanStack Query
  3. Validation Debouncing: Real-time validation is debounced to avoid excessive computation
  4. Component Optimization: ParameterInputs uses React.memo for performance
  5. Bundle Size: Components are tree-shakeable and imported dynamically where possible

Backwards Compatibility

  1. Flow Classes Without Parameters: Existing flow classes continue to work without changes
  2. API Compatibility: Parameter field is optional in API calls
  3. UI Graceful Degradation: UI gracefully handles flows with no parameters
  4. Existing Flows: Existing flows without parameters continue to function normally

Future Enhancements

  1. Parameter Templates: Pre-defined parameter sets for common use cases
  2. Conditional Parameters: Parameters that appear based on other parameter values
  3. Parameter Groups: Organize related parameters into collapsible sections
  4. Import/Export: Bulk parameter configuration via JSON/YAML files
  5. Parameter History: Track and suggest parameter values based on usage patterns
  6. Advanced Validation: Custom validation rules and cross-parameter validation
  7. Parameter Documentation: Rich documentation with examples and best practices