Fix code style

This commit is contained in:
Nandha Reddy 2025-07-07 13:53:10 +10:00
commit 236dee0dca
No known key found for this signature in database
GPG key ID: 09ED40FC0F563333
2 changed files with 396 additions and 379 deletions

View file

@ -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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import JsonEditor from "./JsonEditor"; import JsonEditor from "./JsonEditor";
@ -55,328 +62,129 @@ const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
} }
}; };
const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(({ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
schema, ({ schema, value, onChange, maxDepth = 3 }, ref) => {
value, const isOnlyJSON = !isSimpleObject(schema);
onChange, const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
maxDepth = 3, const [jsonError, setJsonError] = useState<string>();
}, ref) => { // Store the raw JSON string to allow immediate feedback during typing
const isOnlyJSON = !isSimpleObject(schema); // while deferring parsing until the user stops typing
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [rawJsonValue, setRawJsonValue] = useState<string>(
const [jsonError, setJsonError] = useState<string>(); JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
// Store the raw JSON string to allow immediate feedback during typing );
// while deferring parsing until the user stops typing
const [rawJsonValue, setRawJsonValue] = useState<string>(
JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
);
// Use a ref to manage debouncing timeouts to avoid parsing JSON // Use a ref to manage debouncing timeouts to avoid parsing JSON
// on every keystroke which would be inefficient and error-prone // on every keystroke which would be inefficient and error-prone
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(); const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
// Debounce JSON parsing and parent updates to handle typing gracefully // Debounce JSON parsing and parent updates to handle typing gracefully
const debouncedUpdateParent = useCallback( const debouncedUpdateParent = useCallback(
(jsonString: string) => { (jsonString: string) => {
// Clear any existing timeout // Clear any existing timeout
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(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
} }
}, 300);
},
[onChange, setJsonError],
);
// Update rawJsonValue when value prop changes // Set a new timeout
useEffect(() => { timeoutRef.current = setTimeout(() => {
if (!isJsonMode) { try {
setRawJsonValue( const parsed = JSON.parse(jsonString);
JSON.stringify(value ?? generateDefaultValue(schema), null, 2), onChange(parsed);
); setJsonError(undefined);
} } catch {
}, [value, schema, isJsonMode]); // Don't set error during normal typing
}
}, 300);
},
[onChange, setJsonError],
);
const handleSwitchToFormMode = () => { // Update rawJsonValue when value prop changes
if (isJsonMode) { useEffect(() => {
// When switching to Form mode, ensure we have valid JSON 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 { try {
const parsed = JSON.parse(rawJsonValue); const jsonStr = rawJsonValue.trim();
// Update the parent component's state with the parsed value if (!jsonStr) {
onChange(parsed); return;
// Switch to form mode }
setIsJsonMode(false); const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2);
setRawJsonValue(formatted);
debouncedUpdateParent(formatted);
setJsonError(undefined);
} catch (err) { } catch (err) {
setJsonError(err instanceof Error ? err.message : "Invalid JSON"); 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 = () => { const validateJson = () => {
try { if (!isJsonMode) return { isValid: true, error: null };
const jsonStr = rawJsonValue.trim(); try {
if (!jsonStr) { const jsonStr = rawJsonValue.trim();
return; 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 = () => { useImperativeHandle(ref, () => ({
if (!isJsonMode) return { isValid: true, error: null }; validateJson,
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, () => ({ const renderFormFields = (
validateJson, propSchema: JsonSchemaType,
})); currentValue: JsonValue,
path: string[] = [],
const renderFormFields = ( depth: number = 0,
propSchema: JsonSchemaType, parentSchema?: JsonSchemaType,
currentValue: JsonValue, propertyName?: string,
path: string[] = [], ) => {
depth: number = 0, if (
parentSchema?: JsonSchemaType, depth >= maxDepth &&
propertyName?: string, (propSchema.type === "object" || propSchema.type === "array")
) => { ) {
if ( // Render as JSON editor when max depth is reached
depth >= maxDepth &&
(propSchema.type === "object" || propSchema.type === "array")
) {
// Render as JSON editor when max depth is reached
return (
<JsonEditor
value={JSON.stringify(
currentValue ??
generateDefaultValue(propSchema, propertyName, parentSchema),
null,
2,
)}
onChange={(newValue) => {
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 (
<Input
type="text"
value={(currentValue as string) ?? ""}
onChange={(e) => {
const val = e.target.value;
// Always allow setting string values, including empty strings
handleFieldChange(path, val);
}}
placeholder={propSchema.description}
required={isRequired}
/>
);
case "number":
return (
<Input
type="number"
value={(currentValue as number)?.toString() ?? ""}
onChange={(e) => {
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 (
<Input
type="number"
step="1"
value={(currentValue as number)?.toString() ?? ""}
onChange={(e) => {
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 (
<Input
type="checkbox"
checked={(currentValue as boolean) ?? false}
onChange={(e) => handleFieldChange(path, e.target.checked)}
className="w-4 h-4"
required={isRequired}
/>
);
case "object":
if (!propSchema.properties) {
return (
<JsonEditor
value={JSON.stringify(currentValue ?? {}, null, 2)}
onChange={(newValue) => {
try {
const parsed = JSON.parse(newValue);
handleFieldChange(path, parsed);
setJsonError(undefined);
} catch (err) {
setJsonError(
err instanceof Error ? err.message : "Invalid JSON",
);
}
}}
error={jsonError}
/>
);
}
return (
<div className="space-y-2 border rounded p-3">
{Object.entries(propSchema.properties).map(([key, subSchema]) => (
<div key={key}>
<label className="block text-sm font-medium mb-1">
{key}
{propSchema.required?.includes(key) && (
<span className="text-red-500 ml-1">*</span>
)}
</label>
{renderFormFields(
subSchema as JsonSchemaType,
(currentValue as Record<string, JsonValue>)?.[key],
[...path, key],
depth + 1,
propSchema,
key,
)}
</div>
))}
</div>
);
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 (
<div className="space-y-4">
{propSchema.description && (
<p className="text-sm text-gray-600">
{propSchema.description}
</p>
)}
{propSchema.items?.description && (
<p className="text-sm text-gray-500">
Items: {propSchema.items.description}
</p>
)}
<div className="space-y-2">
{arrayValue.map((item, index) => (
<div key={index} className="flex items-center gap-2">
{renderFormFields(
propSchema.items as JsonSchemaType,
item,
[...path, index.toString()],
depth + 1,
)}
<Button
variant="outline"
size="sm"
onClick={() => {
const newArray = [...arrayValue];
newArray.splice(index, 1);
handleFieldChange(path, newArray);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => {
const defaultValue = getArrayItemDefault(
propSchema.items as JsonSchemaType,
);
handleFieldChange(path, [...arrayValue, defaultValue]);
}}
title={
propSchema.items?.description
? `Add new ${propSchema.items.description}`
: "Add new item"
}
>
Add Item
</Button>
</div>
</div>
);
}
// For complex arrays, fall back to JSON editor
return ( return (
<JsonEditor <JsonEditor
value={JSON.stringify(currentValue ?? [], null, 2)} value={JSON.stringify(
currentValue ??
generateDefaultValue(propSchema, propertyName, parentSchema),
null,
2,
)}
onChange={(newValue) => { onChange={(newValue) => {
try { try {
const parsed = JSON.parse(newValue); const parsed = JSON.parse(newValue);
@ -392,93 +200,296 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(({
/> />
); );
} }
default:
return null;
}
};
const handleFieldChange = (path: string[], fieldValue: JsonValue) => { // Check if this property is required in the parent schema
if (path.length === 0) { const isRequired =
onChange(fieldValue); parentSchema?.required?.includes(propertyName || "") ?? false;
return;
}
try { switch (propSchema.type) {
const newValue = updateValueAtPath(value, path, fieldValue); case "string":
onChange(newValue); return (
} catch (error) { <Input
console.error("Failed to update form value:", error); type="text"
onChange(value); value={(currentValue as string) ?? ""}
} onChange={(e) => {
}; const val = e.target.value;
// Always allow setting string values, including empty strings
handleFieldChange(path, val);
}}
placeholder={propSchema.description}
required={isRequired}
/>
);
case "number":
return (
<Input
type="number"
value={(currentValue as number)?.toString() ?? ""}
onChange={(e) => {
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 (
<Input
type="number"
step="1"
value={(currentValue as number)?.toString() ?? ""}
onChange={(e) => {
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 (
<Input
type="checkbox"
checked={(currentValue as boolean) ?? false}
onChange={(e) => handleFieldChange(path, e.target.checked)}
className="w-4 h-4"
required={isRequired}
/>
);
case "object":
if (!propSchema.properties) {
return (
<JsonEditor
value={JSON.stringify(currentValue ?? {}, null, 2)}
onChange={(newValue) => {
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 = return (
schema.type === "object" && <div className="space-y-2 border rounded p-3">
(!schema.properties || Object.keys(schema.properties).length === 0); {Object.entries(propSchema.properties).map(([key, subSchema]) => (
<div key={key}>
<label className="block text-sm font-medium mb-1">
{key}
{propSchema.required?.includes(key) && (
<span className="text-red-500 ml-1">*</span>
)}
</label>
{renderFormFields(
subSchema as JsonSchemaType,
(currentValue as Record<string, JsonValue>)?.[key],
[...path, key],
depth + 1,
propSchema,
key,
)}
</div>
))}
</div>
);
case "array": {
const arrayValue = Array.isArray(currentValue) ? currentValue : [];
if (!propSchema.items) return null;
useEffect(() => { // If the array items are simple, render as form fields, otherwise use JSON editor
if (shouldUseJsonMode && !isJsonMode) { if (isSimpleObject(propSchema.items)) {
setIsJsonMode(true); return (
} <div className="space-y-4">
}, [shouldUseJsonMode, isJsonMode]); {propSchema.description && (
<p className="text-sm text-gray-600">
{propSchema.description}
</p>
)}
return ( {propSchema.items?.description && (
<div className="space-y-4"> <p className="text-sm text-gray-500">
<div className="flex justify-end space-x-2"> Items: {propSchema.items.description}
{isJsonMode && ( </p>
<Button )}
type="button"
variant="outline" <div className="space-y-2">
size="sm" {arrayValue.map((item, index) => (
onClick={formatJson} <div key={index} className="flex items-center gap-2">
> {renderFormFields(
Format JSON propSchema.items as JsonSchemaType,
</Button> item,
)} [...path, index.toString()],
{!isOnlyJSON && ( depth + 1,
<Button variant="outline" size="sm" onClick={handleSwitchToFormMode}> )}
{isJsonMode ? "Switch to Form" : "Switch to JSON"} <Button
</Button> variant="outline"
size="sm"
onClick={() => {
const newArray = [...arrayValue];
newArray.splice(index, 1);
handleFieldChange(path, newArray);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => {
const defaultValue = getArrayItemDefault(
propSchema.items as JsonSchemaType,
);
handleFieldChange(path, [...arrayValue, defaultValue]);
}}
title={
propSchema.items?.description
? `Add new ${propSchema.items.description}`
: "Add new item"
}
>
Add Item
</Button>
</div>
</div>
);
}
// For complex arrays, fall back to JSON editor
return (
<JsonEditor
value={JSON.stringify(currentValue ?? [], null, 2)}
onChange={(newValue) => {
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 (
<div className="space-y-4">
<div className="flex justify-end space-x-2">
{isJsonMode && (
<Button
type="button"
variant="outline"
size="sm"
onClick={formatJson}
>
Format JSON
</Button>
)}
{!isOnlyJSON && (
<Button
variant="outline"
size="sm"
onClick={handleSwitchToFormMode}
>
{isJsonMode ? "Switch to Form" : "Switch to JSON"}
</Button>
)}
</div>
{isJsonMode ? (
<JsonEditor
value={rawJsonValue}
onChange={(newValue) => {
// 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 !== "{}" ? (
<div className="space-y-4 border rounded-md p-4">
<p className="text-sm text-gray-500">
Form view not available for this JSON structure. Using simplified
view:
</p>
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto">
{rawJsonValue}
</pre>
<p className="text-sm text-gray-500">
Use JSON mode for full editing capabilities.
</p>
</div>
) : (
renderFormFields(schema, value)
)} )}
</div> </div>
);
{isJsonMode ? ( },
<JsonEditor );
value={rawJsonValue}
onChange={(newValue) => {
// 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 !== "{}" ? (
<div className="space-y-4 border rounded-md p-4">
<p className="text-sm text-gray-500">
Form view not available for this JSON structure. Using simplified
view:
</p>
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto">
{rawJsonValue}
</pre>
<p className="text-sm text-gray-500">
Use JSON mode for full editing capabilities.
</p>
</div>
) : (
renderFormFields(schema, value)
)}
</div>
);
});
export default DynamicJsonForm; export default DynamicJsonForm;

View file

@ -13,7 +13,13 @@ import {
ListToolsResult, ListToolsResult,
Tool, Tool,
} from "@modelcontextprotocol/sdk/types.js"; } 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 { useEffect, useState, useRef } from "react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import JsonView from "./JsonView"; import JsonView from "./JsonView";
@ -254,9 +260,9 @@ const ToolsTab = ({
<Button <Button
onClick={async () => { onClick={async () => {
// Validate JSON inputs before calling tool // Validate JSON inputs before calling tool
const hasValidationErrors = Object.values(formRefs.current).some( const hasValidationErrors = Object.values(
(ref) => ref && !ref.validateJson().isValid formRefs.current,
); ).some((ref) => ref && !ref.validateJson().isValid);
if (hasValidationErrors) return; if (hasValidationErrors) return;
try { try {