import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ToolInputSchema } from '@/types'; interface JsonSchema { type: string; properties?: Record; required?: string[]; items?: JsonSchema; enum?: any[]; description?: string; default?: any; } interface DynamicFormProps { schema: ToolInputSchema; onSubmit: (values: Record) => void; onCancel: () => void; loading?: boolean; storageKey?: string; // Optional key for localStorage persistence title?: string; // Optional title to display instead of default parameters title } const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, loading = false, storageKey, title, }) => { const { t } = useTranslation(); const [formValues, setFormValues] = useState>({}); const [errors, setErrors] = useState>({}); const [isJsonMode, setIsJsonMode] = useState(false); const [jsonText, setJsonText] = useState(''); const [jsonError, setJsonError] = useState(''); // Convert ToolInputSchema to JsonSchema - memoized to prevent infinite re-renders const jsonSchema = useMemo(() => { const convertToJsonSchema = (schema: ToolInputSchema): JsonSchema => { const convertProperty = (prop: unknown): JsonSchema => { if (typeof prop === 'object' && prop !== null) { const obj = prop as any; return { type: obj.type || 'string', description: obj.description, enum: obj.enum, default: obj.default, properties: obj.properties ? Object.fromEntries( Object.entries(obj.properties).map(([key, value]) => [ key, convertProperty(value), ]), ) : undefined, required: obj.required, items: obj.items ? convertProperty(obj.items) : undefined, }; } return { type: 'string' }; }; return { type: schema.type, properties: schema.properties ? Object.fromEntries( Object.entries(schema.properties).map(([key, value]) => [ key, convertProperty(value), ]), ) : undefined, required: schema.required, }; }; return convertToJsonSchema(schema); }, [schema]); // Initialize form values with defaults or from localStorage useEffect(() => { const initializeValues = (schema: JsonSchema, path: string = ''): Record => { const values: Record = {}; if (schema.type === 'object' && schema.properties) { Object.entries(schema.properties).forEach(([key, propSchema]) => { const fullPath = path ? `${path}.${key}` : key; if (propSchema.default !== undefined) { values[key] = propSchema.default; } else if (propSchema.type === 'string') { values[key] = ''; } else if (propSchema.type === 'number' || propSchema.type === 'integer') { values[key] = 0; } else if (propSchema.type === 'boolean') { values[key] = false; } else if (propSchema.type === 'array') { values[key] = []; } else if (propSchema.type === 'object') { // For objects with properties, recursively initialize if (propSchema.properties) { values[key] = initializeValues(propSchema, fullPath); } else { // For objects without properties, initialize as empty object values[key] = {}; } } }); } return values; }; let initialValues = initializeValues(jsonSchema); // Try to load saved form data from localStorage if (storageKey) { try { const savedData = localStorage.getItem(storageKey); if (savedData) { const parsedData = JSON.parse(savedData); // Merge saved data with initial values, preserving structure initialValues = { ...initialValues, ...parsedData }; } } catch (error) { console.warn('Failed to load saved form data:', error); } } setFormValues(initialValues); }, [jsonSchema, storageKey]); // Sync JSON text with form values when switching modes useEffect(() => { if (isJsonMode && Object.keys(formValues).length > 0) { setJsonText(JSON.stringify(formValues, null, 2)); setJsonError(''); } }, [isJsonMode, formValues]); const handleJsonTextChange = (text: string) => { setJsonText(text); setJsonError(''); try { const parsedJson = JSON.parse(text); setFormValues(parsedJson); // Save to localStorage if storageKey is provided if (storageKey) { try { localStorage.setItem(storageKey, JSON.stringify(parsedJson)); } catch (error) { console.warn('Failed to save form data to localStorage:', error); } } } catch (error) { setJsonError(t('tool.invalidJsonFormat')); } }; const switchToJsonMode = () => { setJsonText(JSON.stringify(formValues, null, 2)); setJsonError(''); setIsJsonMode(true); }; const switchToFormMode = () => { // Validate JSON before switching if (jsonText.trim()) { try { const parsedJson = JSON.parse(jsonText); setFormValues(parsedJson); setJsonError(''); setIsJsonMode(false); } catch (error) { setJsonError(t('tool.fixJsonBeforeSwitching')); return; } } else { setIsJsonMode(false); } }; const handleInputChange = (path: string, value: any) => { setFormValues((prev) => { const newValues = { ...prev }; const keys = path.split('.'); let current = newValues; for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) { current[keys[i]] = {}; } current = current[keys[i]]; } current[keys[keys.length - 1]] = value; // Save to localStorage if storageKey is provided if (storageKey) { try { localStorage.setItem(storageKey, JSON.stringify(newValues)); } catch (error) { console.warn('Failed to save form data to localStorage:', error); } } return newValues; }); // Clear error for this field if (errors[path]) { setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[path]; return newErrors; }); } }; const validateForm = (): boolean => { const newErrors: Record = {}; const validateObject = (schema: JsonSchema, values: any, path: string = '') => { if (schema.type === 'object' && schema.properties) { Object.entries(schema.properties).forEach(([key, propSchema]) => { const fullPath = path ? `${path}.${key}` : key; const value = values?.[key]; // Check required fields if ( schema.required?.includes(key) && (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) ) { newErrors[fullPath] = `${key} is required`; return; } // Validate type if (value !== undefined && value !== null && value !== '') { if (propSchema.type === 'string' && typeof value !== 'string') { newErrors[fullPath] = `${key} must be a string`; } else if (propSchema.type === 'number' && typeof value !== 'number') { newErrors[fullPath] = `${key} must be a number`; } else if ( propSchema.type === 'integer' && (!Number.isInteger(value) || typeof value !== 'number') ) { newErrors[fullPath] = `${key} must be an integer`; } else if (propSchema.type === 'boolean' && typeof value !== 'boolean') { newErrors[fullPath] = `${key} must be a boolean`; } else if (propSchema.type === 'array' && Array.isArray(value)) { // Validate array items if (propSchema.items) { value.forEach((item: any, index: number) => { if (propSchema.items?.type === 'object' && propSchema.items.properties) { validateObject(propSchema.items, item, `${fullPath}.${index}`); } }); } } else if (propSchema.type === 'object' && typeof value === 'object') { validateObject(propSchema, value, fullPath); } } }); } }; validateObject(jsonSchema, formValues); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (validateForm()) { onSubmit(formValues); } }; const getNestedValue = (obj: any, path: string): any => { return path.split('.').reduce((current, key) => current?.[key], obj); }; const renderObjectField = ( key: string, schema: JsonSchema, currentValue: any, onChange: (value: any) => void, ): React.ReactNode => { const value = currentValue?.[key]; if (schema.type === 'string') { if (schema.enum) { return ( ); } else { return ( onChange(e.target.value)} className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 form-input" placeholder={schema.description || t('tool.enterKey', { key })} /> ); } } if (schema.type === 'number' || schema.type === 'integer') { return ( { const val = e.target.value === '' ? '' : schema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value); onChange(val); }} className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 form-input" /> ); } if (schema.type === 'boolean') { return ( onChange(e.target.checked)} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" /> ); } // Default to text input return ( onChange(e.target.value)} className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 form-input" placeholder={schema.description || t('tool.enterKey', { key })} /> ); }; const renderField = (key: string, propSchema: JsonSchema, path: string = ''): React.ReactNode => { const fullPath = path ? `${path}.${key}` : key; const value = getNestedValue(formValues, fullPath); const error = errors[fullPath]; // Handle array type if (propSchema.type === 'array') { const arrayValue = getNestedValue(formValues, fullPath) || []; return (
{propSchema.description && (

{propSchema.description}

)}
{arrayValue.map((item: any, index: number) => (
{t('tool.item', { index: index + 1 })}
{propSchema.items?.type === 'string' && propSchema.items.enum ? ( ) : propSchema.items?.type === 'object' && propSchema.items.properties ? (
{Object.entries(propSchema.items.properties).map(([objKey, objSchema]) => (
{renderObjectField(objKey, objSchema as JsonSchema, item, (newValue) => { const newArray = [...arrayValue]; newArray[index] = { ...newArray[index], [objKey]: newValue }; handleInputChange(fullPath, newArray); })}
))}
) : ( { const newArray = [...arrayValue]; newArray[index] = e.target.value; handleInputChange(fullPath, newArray); }} className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input" placeholder={t('tool.enterValue', { type: propSchema.items?.type || 'value' })} /> )}
))}
{error &&

{error}

}
); } // Handle object type if (propSchema.type === 'object') { if (propSchema.properties) { // Object with defined properties - render as nested form return (
{propSchema.description && (

{propSchema.description}

)}
{Object.entries(propSchema.properties).map(([objKey, objSchema]) => renderField(objKey, objSchema as JsonSchema, fullPath), )}
{error &&

{error}

}
); } else { // Object without defined properties - render as JSON textarea return (
{propSchema.description && (

{propSchema.description}

)}