diff --git a/frontend/src/components/ui/DynamicForm.tsx b/frontend/src/components/ui/DynamicForm.tsx index d161765..690c206 100644 --- a/frontend/src/components/ui/DynamicForm.tsx +++ b/frontend/src/components/ui/DynamicForm.tsx @@ -24,6 +24,9 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l 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(() => { @@ -77,7 +80,13 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l } else if (propSchema.type === 'array') { values[key] = []; } else if (propSchema.type === 'object') { - values[key] = initializeValues(propSchema, fullPath); + // 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] = {}; + } } }); } @@ -104,6 +113,58 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l 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 }; @@ -140,7 +201,6 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l }); } }; - const validateForm = (): boolean => { const newErrors: Record = {}; @@ -148,7 +208,7 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l if (schema.type === 'object' && schema.properties) { Object.entries(schema.properties).forEach(([key, propSchema]) => { const fullPath = path ? `${path}.${key}` : key; - const value = values?.[key]; + const value = getNestedValue(values, fullPath); // Check required fields if (schema.required?.includes(key) && (value === undefined || value === null || value === '')) { @@ -166,6 +226,15 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l 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); } @@ -186,18 +255,240 @@ const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, l } }; + 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" + 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" + /> + ); + } + + 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" + 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 = formValues[key]; - const error = errors[fullPath]; + const value = getNestedValue(formValues, fullPath); + const error = errors[fullPath]; // Handle array type + if (propSchema.type === 'array') { + const arrayValue = getNestedValue(formValues, fullPath) || []; - if (propSchema.type === 'string') { + 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" + 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}

+ )} +