diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index bf16cf3f..8b339980 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -1,4 +1,11 @@ -import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from "react"; +import { + useState, + useEffect, + useCallback, + useRef, + forwardRef, + useImperativeHandle, +} from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import JsonEditor from "./JsonEditor"; @@ -55,328 +62,129 @@ const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => { } }; -const DynamicJsonForm = forwardRef(({ - schema, - value, - onChange, - maxDepth = 3, -}, ref) => { - const isOnlyJSON = !isSimpleObject(schema); - const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); - const [jsonError, setJsonError] = useState(); - // Store the raw JSON string to allow immediate feedback during typing - // while deferring parsing until the user stops typing - const [rawJsonValue, setRawJsonValue] = useState( - JSON.stringify(value ?? generateDefaultValue(schema), null, 2), - ); +const DynamicJsonForm = forwardRef( + ({ schema, value, onChange, maxDepth = 3 }, ref) => { + const isOnlyJSON = !isSimpleObject(schema); + const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); + const [jsonError, setJsonError] = useState(); + // Store the raw JSON string to allow immediate feedback during typing + // while deferring parsing until the user stops typing + const [rawJsonValue, setRawJsonValue] = useState( + JSON.stringify(value ?? generateDefaultValue(schema), null, 2), + ); - // Use a ref to manage debouncing timeouts to avoid parsing JSON - // on every keystroke which would be inefficient and error-prone - const timeoutRef = useRef>(); + // Use a ref to manage debouncing timeouts to avoid parsing JSON + // on every keystroke which would be inefficient and error-prone + const timeoutRef = useRef>(); - // Debounce JSON parsing and parent updates to handle typing gracefully - const debouncedUpdateParent = useCallback( - (jsonString: string) => { - // Clear any existing timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - // Set a new timeout - timeoutRef.current = setTimeout(() => { - try { - const parsed = JSON.parse(jsonString); - onChange(parsed); - setJsonError(undefined); - } catch { - // Don't set error during normal typing + // Debounce JSON parsing and parent updates to handle typing gracefully + const debouncedUpdateParent = useCallback( + (jsonString: string) => { + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); } - }, 300); - }, - [onChange, setJsonError], - ); - // Update rawJsonValue when value prop changes - useEffect(() => { - if (!isJsonMode) { - setRawJsonValue( - JSON.stringify(value ?? generateDefaultValue(schema), null, 2), - ); - } - }, [value, schema, isJsonMode]); + // Set a new timeout + timeoutRef.current = setTimeout(() => { + try { + const parsed = JSON.parse(jsonString); + onChange(parsed); + setJsonError(undefined); + } catch { + // Don't set error during normal typing + } + }, 300); + }, + [onChange, setJsonError], + ); - const handleSwitchToFormMode = () => { - if (isJsonMode) { - // When switching to Form mode, ensure we have valid JSON + // Update rawJsonValue when value prop changes + useEffect(() => { + if (!isJsonMode) { + setRawJsonValue( + JSON.stringify(value ?? generateDefaultValue(schema), null, 2), + ); + } + }, [value, schema, isJsonMode]); + + const handleSwitchToFormMode = () => { + if (isJsonMode) { + // When switching to Form mode, ensure we have valid JSON + try { + const parsed = JSON.parse(rawJsonValue); + // Update the parent component's state with the parsed value + onChange(parsed); + // Switch to form mode + setIsJsonMode(false); + } catch (err) { + setJsonError(err instanceof Error ? err.message : "Invalid JSON"); + } + } else { + // Update raw JSON value when switching to JSON mode + setRawJsonValue( + JSON.stringify(value ?? generateDefaultValue(schema), null, 2), + ); + setIsJsonMode(true); + } + }; + + const formatJson = () => { try { - const parsed = JSON.parse(rawJsonValue); - // Update the parent component's state with the parsed value - onChange(parsed); - // Switch to form mode - setIsJsonMode(false); + const jsonStr = rawJsonValue.trim(); + if (!jsonStr) { + return; + } + const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2); + setRawJsonValue(formatted); + debouncedUpdateParent(formatted); + setJsonError(undefined); } catch (err) { setJsonError(err instanceof Error ? err.message : "Invalid JSON"); } - } else { - // Update raw JSON value when switching to JSON mode - setRawJsonValue( - JSON.stringify(value ?? generateDefaultValue(schema), null, 2), - ); - setIsJsonMode(true); - } - }; + }; - const formatJson = () => { - try { - const jsonStr = rawJsonValue.trim(); - if (!jsonStr) { - return; + const validateJson = () => { + if (!isJsonMode) return { isValid: true, error: null }; + try { + const jsonStr = rawJsonValue.trim(); + if (!jsonStr) return { isValid: true, error: null }; + JSON.parse(jsonStr); + setJsonError(undefined); + return { isValid: true, error: null }; + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Invalid JSON"; + setJsonError(errorMessage); + return { isValid: false, error: errorMessage }; } - const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2); - setRawJsonValue(formatted); - debouncedUpdateParent(formatted); - setJsonError(undefined); - } catch (err) { - setJsonError(err instanceof Error ? err.message : "Invalid JSON"); - } - }; + }; - const validateJson = () => { - if (!isJsonMode) return { isValid: true, error: null }; - try { - const jsonStr = rawJsonValue.trim(); - if (!jsonStr) return { isValid: true, error: null }; - JSON.parse(jsonStr); - setJsonError(undefined); - return { isValid: true, error: null }; - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Invalid JSON"; - setJsonError(errorMessage); - return { isValid: false, error: errorMessage }; - } - }; + useImperativeHandle(ref, () => ({ + validateJson, + })); - useImperativeHandle(ref, () => ({ - validateJson, - })); - - const renderFormFields = ( - propSchema: JsonSchemaType, - currentValue: JsonValue, - path: string[] = [], - depth: number = 0, - parentSchema?: JsonSchemaType, - propertyName?: string, - ) => { - if ( - depth >= maxDepth && - (propSchema.type === "object" || propSchema.type === "array") - ) { - // Render as JSON editor when max depth is reached - return ( - { - try { - const parsed = JSON.parse(newValue); - handleFieldChange(path, parsed); - setJsonError(undefined); - } catch (err) { - setJsonError(err instanceof Error ? err.message : "Invalid JSON"); - } - }} - error={jsonError} - /> - ); - } - - // Check if this property is required in the parent schema - const isRequired = - parentSchema?.required?.includes(propertyName || "") ?? false; - - switch (propSchema.type) { - case "string": - return ( - { - const val = e.target.value; - // Always allow setting string values, including empty strings - handleFieldChange(path, val); - }} - placeholder={propSchema.description} - required={isRequired} - /> - ); - case "number": - return ( - { - const val = e.target.value; - if (!val && !isRequired) { - handleFieldChange(path, undefined); - } else { - const num = Number(val); - if (!isNaN(num)) { - handleFieldChange(path, num); - } - } - }} - placeholder={propSchema.description} - required={isRequired} - /> - ); - case "integer": - return ( - { - const val = e.target.value; - if (!val && !isRequired) { - handleFieldChange(path, undefined); - } else { - const num = Number(val); - // Only update if it's a valid integer - if (!isNaN(num) && Number.isInteger(num)) { - handleFieldChange(path, num); - } - } - }} - placeholder={propSchema.description} - required={isRequired} - /> - ); - case "boolean": - return ( - handleFieldChange(path, e.target.checked)} - className="w-4 h-4" - required={isRequired} - /> - ); - case "object": - if (!propSchema.properties) { - return ( - { - try { - const parsed = JSON.parse(newValue); - handleFieldChange(path, parsed); - setJsonError(undefined); - } catch (err) { - setJsonError( - err instanceof Error ? err.message : "Invalid JSON", - ); - } - }} - error={jsonError} - /> - ); - } - - return ( -
- {Object.entries(propSchema.properties).map(([key, subSchema]) => ( -
- - {renderFormFields( - subSchema as JsonSchemaType, - (currentValue as Record)?.[key], - [...path, key], - depth + 1, - propSchema, - key, - )} -
- ))} -
- ); - case "array": { - const arrayValue = Array.isArray(currentValue) ? currentValue : []; - if (!propSchema.items) return null; - - // If the array items are simple, render as form fields, otherwise use JSON editor - if (isSimpleObject(propSchema.items)) { - return ( -
- {propSchema.description && ( -

- {propSchema.description} -

- )} - - {propSchema.items?.description && ( -

- Items: {propSchema.items.description} -

- )} - -
- {arrayValue.map((item, index) => ( -
- {renderFormFields( - propSchema.items as JsonSchemaType, - item, - [...path, index.toString()], - depth + 1, - )} - -
- ))} - -
-
- ); - } - - // For complex arrays, fall back to JSON editor + const renderFormFields = ( + propSchema: JsonSchemaType, + currentValue: JsonValue, + path: string[] = [], + depth: number = 0, + parentSchema?: JsonSchemaType, + propertyName?: string, + ) => { + if ( + depth >= maxDepth && + (propSchema.type === "object" || propSchema.type === "array") + ) { + // Render as JSON editor when max depth is reached return ( { try { const parsed = JSON.parse(newValue); @@ -392,93 +200,296 @@ const DynamicJsonForm = forwardRef(({ /> ); } - default: - return null; - } - }; - const handleFieldChange = (path: string[], fieldValue: JsonValue) => { - if (path.length === 0) { - onChange(fieldValue); - return; - } + // Check if this property is required in the parent schema + const isRequired = + parentSchema?.required?.includes(propertyName || "") ?? false; - try { - const newValue = updateValueAtPath(value, path, fieldValue); - onChange(newValue); - } catch (error) { - console.error("Failed to update form value:", error); - onChange(value); - } - }; + switch (propSchema.type) { + case "string": + return ( + { + const val = e.target.value; + // Always allow setting string values, including empty strings + handleFieldChange(path, val); + }} + placeholder={propSchema.description} + required={isRequired} + /> + ); + case "number": + return ( + { + const val = e.target.value; + if (!val && !isRequired) { + handleFieldChange(path, undefined); + } else { + const num = Number(val); + if (!isNaN(num)) { + handleFieldChange(path, num); + } + } + }} + placeholder={propSchema.description} + required={isRequired} + /> + ); + case "integer": + return ( + { + const val = e.target.value; + if (!val && !isRequired) { + handleFieldChange(path, undefined); + } else { + const num = Number(val); + // Only update if it's a valid integer + if (!isNaN(num) && Number.isInteger(num)) { + handleFieldChange(path, num); + } + } + }} + placeholder={propSchema.description} + required={isRequired} + /> + ); + case "boolean": + return ( + handleFieldChange(path, e.target.checked)} + className="w-4 h-4" + required={isRequired} + /> + ); + case "object": + if (!propSchema.properties) { + return ( + { + try { + const parsed = JSON.parse(newValue); + handleFieldChange(path, parsed); + setJsonError(undefined); + } catch (err) { + setJsonError( + err instanceof Error ? err.message : "Invalid JSON", + ); + } + }} + error={jsonError} + /> + ); + } - const shouldUseJsonMode = - schema.type === "object" && - (!schema.properties || Object.keys(schema.properties).length === 0); + return ( +
+ {Object.entries(propSchema.properties).map(([key, subSchema]) => ( +
+ + {renderFormFields( + subSchema as JsonSchemaType, + (currentValue as Record)?.[key], + [...path, key], + depth + 1, + propSchema, + key, + )} +
+ ))} +
+ ); + case "array": { + const arrayValue = Array.isArray(currentValue) ? currentValue : []; + if (!propSchema.items) return null; - useEffect(() => { - if (shouldUseJsonMode && !isJsonMode) { - setIsJsonMode(true); - } - }, [shouldUseJsonMode, isJsonMode]); + // If the array items are simple, render as form fields, otherwise use JSON editor + if (isSimpleObject(propSchema.items)) { + return ( +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} - return ( -
-
- {isJsonMode && ( - - )} - {!isOnlyJSON && ( - + {propSchema.items?.description && ( +

+ Items: {propSchema.items.description} +

+ )} + +
+ {arrayValue.map((item, index) => ( +
+ {renderFormFields( + propSchema.items as JsonSchemaType, + item, + [...path, index.toString()], + depth + 1, + )} + +
+ ))} + +
+
+ ); + } + + // For complex arrays, fall back to JSON editor + return ( + { + try { + const parsed = JSON.parse(newValue); + handleFieldChange(path, parsed); + setJsonError(undefined); + } catch (err) { + setJsonError( + err instanceof Error ? err.message : "Invalid JSON", + ); + } + }} + error={jsonError} + /> + ); + } + default: + return null; + } + }; + + const handleFieldChange = (path: string[], fieldValue: JsonValue) => { + if (path.length === 0) { + onChange(fieldValue); + return; + } + + try { + const newValue = updateValueAtPath(value, path, fieldValue); + onChange(newValue); + } catch (error) { + console.error("Failed to update form value:", error); + onChange(value); + } + }; + + const shouldUseJsonMode = + schema.type === "object" && + (!schema.properties || Object.keys(schema.properties).length === 0); + + useEffect(() => { + if (shouldUseJsonMode && !isJsonMode) { + setIsJsonMode(true); + } + }, [shouldUseJsonMode, isJsonMode]); + + return ( +
+
+ {isJsonMode && ( + + )} + {!isOnlyJSON && ( + + )} +
+ + {isJsonMode ? ( + { + // Always update local state + setRawJsonValue(newValue); + + // Use the debounced function to attempt parsing and updating parent + debouncedUpdateParent(newValue); + }} + error={jsonError} + /> + ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data, + // render a simple representation of the JSON data + schema.type === "object" && + (typeof value !== "object" || + value === null || + Object.keys(value).length === 0) && + rawJsonValue && + rawJsonValue !== "{}" ? ( +
+

+ Form view not available for this JSON structure. Using simplified + view: +

+
+              {rawJsonValue}
+            
+

+ Use JSON mode for full editing capabilities. +

+
+ ) : ( + renderFormFields(schema, value) )}
- - {isJsonMode ? ( - { - // Always update local state - setRawJsonValue(newValue); - - // Use the debounced function to attempt parsing and updating parent - debouncedUpdateParent(newValue); - }} - error={jsonError} - /> - ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data, - // render a simple representation of the JSON data - schema.type === "object" && - (typeof value !== "object" || - value === null || - Object.keys(value).length === 0) && - rawJsonValue && - rawJsonValue !== "{}" ? ( -
-

- Form view not available for this JSON structure. Using simplified - view: -

-
-            {rawJsonValue}
-          
-

- Use JSON mode for full editing capabilities. -

-
- ) : ( - renderFormFields(schema, value) - )} -
- ); -}); + ); + }, +); export default DynamicJsonForm; diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 9f0d2008..7258eaa8 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -13,7 +13,13 @@ import { ListToolsResult, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { Loader2, Send, ChevronDown, ChevronUp, AlertCircle } from "lucide-react"; +import { + Loader2, + Send, + ChevronDown, + ChevronUp, + AlertCircle, +} from "lucide-react"; import { useEffect, useState, useRef } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; @@ -254,9 +260,9 @@ const ToolsTab = ({