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" ? (
- ),
+ )}
+
+ );
+ }
)}