From a4469f789586bface27eedd1f9f9121fb0962cae Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 27 Jan 2025 21:05:11 -0700 Subject: [PATCH 1/2] Add draft version of enhanced object input editing --- client/package.json | 3 + client/src/components/DynamicJsonForm.tsx | 230 ++++++++++++++++++++++ client/src/components/JsonEditor.tsx | 61 ++++++ client/src/components/ToolsTab.tsx | 84 ++++---- package-lock.json | 28 +++ server/package.json | 2 +- 6 files changed, 363 insertions(+), 45 deletions(-) create mode 100644 client/src/components/DynamicJsonForm.tsx create mode 100644 client/src/components/JsonEditor.tsx diff --git a/client/package.json b/client/package.json index 888495c..cadfca9 100644 --- a/client/package.json +++ b/client/package.json @@ -27,11 +27,14 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@types/prismjs": "^1.26.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.447.0", + "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-simple-code-editor": "^0.14.1", "react-toastify": "^10.0.6", "serve-handler": "^6.1.6", "tailwind-merge": "^2.5.3", diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx new file mode 100644 index 0000000..c1a85d5 --- /dev/null +++ b/client/src/components/DynamicJsonForm.tsx @@ -0,0 +1,230 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import JsonEditor from "./JsonEditor"; + +export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + +export type JsonSchemaType = { + type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'; + description?: string; + properties?: Record; + items?: JsonSchemaType; +}; + +type JsonObject = { [key: string]: JsonValue }; + +interface DynamicJsonFormProps { + schema: JsonSchemaType; + value: JsonValue; + onChange: (value: JsonValue) => void; + maxDepth?: number; +} + +const formatFieldLabel = (key: string): string => { + return key + .replace(/([A-Z])/g, ' $1') // Insert space before capital letters + .replace(/_/g, ' ') // Replace underscores with spaces + .replace(/^\w/, c => c.toUpperCase()); // Capitalize first letter +}; + +const DynamicJsonForm = ({ + schema, + value, + onChange, + maxDepth = 3 +}: DynamicJsonFormProps) => { + const [isJsonMode, setIsJsonMode] = useState(false); + const [jsonError, setJsonError] = useState(); + + const generateDefaultValue = (propSchema: JsonSchemaType): JsonValue => { + switch (propSchema.type) { + case 'string': + return ''; + case 'number': + case 'integer': + return 0; + case 'boolean': + return false; + case 'array': + return []; + case 'object': { + const obj: JsonObject = {}; + if (propSchema.properties) { + Object.entries(propSchema.properties).forEach(([key, prop]) => { + obj[key] = generateDefaultValue(prop); + }); + } + return obj; + } + default: + return null; + } + }; + + const renderFormFields = ( + propSchema: JsonSchemaType, + currentValue: JsonValue, + path: string[] = [], + depth: number = 0 + ) => { + 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} + /> + ); + } + + switch (propSchema.type) { + case 'string': + case 'number': + case 'integer': + return ( + handleFieldChange(path, + propSchema.type === 'string' ? e.target.value : Number(e.target.value) + )} + placeholder={propSchema.description} + /> + ); + case 'boolean': + return ( + handleFieldChange(path, e.target.checked)} + className="w-4 h-4" + /> + ); + case 'object': + if (!propSchema.properties) return null; + return ( +
+ {Object.entries(propSchema.properties).map(([key, prop]) => ( +
+ + {renderFormFields( + prop, + (currentValue as JsonObject)?.[key], + [...path, key], + depth + 1 + )} +
+ ))} +
+ ); + case 'array': { + const arrayValue = Array.isArray(currentValue) ? currentValue : []; + if (!propSchema.items) return null; + return ( +
+ {arrayValue.map((item, index) => ( +
+ {renderFormFields( + propSchema.items as JsonSchemaType, + item, + [...path, index.toString()], + depth + 1 + )} + +
+ ))} + +
+ ); + } + default: + return null; + } + }; + + const handleFieldChange = (path: string[], fieldValue: JsonValue) => { + if (path.length === 0) { + onChange(fieldValue); + return; + } + + const newValue = { ...(typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {}) } as JsonObject; + let current: JsonObject = newValue; + + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]; + if (!(key in current)) { + current[key] = {}; + } + current = current[key] as JsonObject; + } + + current[path[path.length - 1]] = fieldValue; + onChange(newValue); + }; + + return ( +
+
+ +
+ + {isJsonMode ? ( + { + try { + onChange(JSON.parse(newValue)); + setJsonError(undefined); + } catch (err) { + setJsonError(err instanceof Error ? err.message : 'Invalid JSON'); + } + }} + error={jsonError} + /> + ) : ( + renderFormFields(schema, value) + )} +
+ ); +}; + +export default DynamicJsonForm; diff --git a/client/src/components/JsonEditor.tsx b/client/src/components/JsonEditor.tsx new file mode 100644 index 0000000..e9d90dc --- /dev/null +++ b/client/src/components/JsonEditor.tsx @@ -0,0 +1,61 @@ +import Editor from 'react-simple-code-editor'; +import Prism from 'prismjs'; +import 'prismjs/components/prism-json'; +import 'prismjs/themes/prism.css'; +import { Button } from "@/components/ui/button"; + +interface JsonEditorProps { + value: string; + onChange: (value: string) => void; + error?: string; +} + +const JsonEditor = ({ value, onChange, error }: JsonEditorProps) => { + const formatJson = (json: string): string => { + try { + return JSON.stringify(JSON.parse(json), null, 2); + } catch { + return json; + } + }; + + return ( +
+
+ +
+
+ + Prism.highlight(code, Prism.languages.json, 'json') + } + padding={10} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 14, + backgroundColor: 'transparent', + minHeight: '100px', + }} + className="w-full" + /> +
+ {error && ( +

{error}

+ )} +
+ ); +}; + +export default JsonEditor; diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 6b64c01..8515d70 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -4,6 +4,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm"; import { ListToolsResult, Tool, @@ -15,6 +16,12 @@ import ListPane from "./ListPane"; import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js"; +type SchemaProperty = { + type: string; + description?: string; + properties?: Record; +}; + const ToolsTab = ({ tools, listTools, @@ -150,22 +157,21 @@ const ToolsTab = ({ {selectedTool.description}

{Object.entries(selectedTool.inputSchema.properties ?? []).map( - ([key, value]) => ( -
- - { - /* @ts-expect-error value type is currently unknown */ - value.type === "string" ? ( + ([key, value]) => { + const prop = value as SchemaProperty; + return ( +
+ + {prop.type === "string" ? (