diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 7da1762..f5b0d63 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -108,6 +108,21 @@ const DynamicJsonForm = ({ } }; + const formatJson = () => { + try { + 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"); + } + }; + const renderFormFields = ( propSchema: JsonSchemaType, currentValue: JsonValue, @@ -353,7 +368,12 @@ const DynamicJsonForm = ({ return (
-
+
+ {isJsonMode && ( + + )} diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index 532d3b1..b03d1f4 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -1,6 +1,7 @@ import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; import { Copy } from "lucide-react"; import { useState } from "react"; +import JsonView from "./JsonView"; const HistoryAndNotifications = ({ requestHistory, @@ -74,9 +75,9 @@ const HistoryAndNotifications = ({
-
-                          {JSON.stringify(JSON.parse(request.request), null, 2)}
-                        
+
+ +
{request.response && (
@@ -91,13 +92,9 @@ const HistoryAndNotifications = ({
-
-                            {JSON.stringify(
-                              JSON.parse(request.response),
-                              null,
-                              2,
-                            )}
-                          
+
+ +
)} @@ -146,9 +143,11 @@ const HistoryAndNotifications = ({ -
-                        {JSON.stringify(notification, null, 2)}
-                      
+
+ +
)} diff --git a/client/src/components/JsonEditor.tsx b/client/src/components/JsonEditor.tsx index 0d2596e..215d66b 100644 --- a/client/src/components/JsonEditor.tsx +++ b/client/src/components/JsonEditor.tsx @@ -3,7 +3,6 @@ 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; @@ -16,49 +15,25 @@ const JsonEditor = ({ onChange, error: externalError, }: JsonEditorProps) => { - const [editorContent, setEditorContent] = useState(value); + const [editorContent, setEditorContent] = useState(value || ""); const [internalError, setInternalError] = useState( undefined, ); useEffect(() => { - setEditorContent(value); + setEditorContent(value || ""); }, [value]); - const formatJson = (json: string): string => { - try { - return JSON.stringify(JSON.parse(json), null, 2); - } catch { - return json; - } - }; - const handleEditorChange = (newContent: string) => { setEditorContent(newContent); setInternalError(undefined); onChange(newContent); }; - const handleFormatJson = () => { - try { - const formatted = formatJson(editorContent); - setEditorContent(formatted); - onChange(formatted); - setInternalError(undefined); - } catch (err) { - setInternalError(err instanceof Error ? err.message : "Invalid JSON"); - } - }; - const displayError = internalError || externalError; return ( -
-
- -
+
{ + const normalizedData = + typeof data === "string" + ? tryParseJson(data).success + ? tryParseJson(data).data + : data + : data; + + return ( +
+ +
+ ); + }, +); + +JsonView.displayName = "JsonView"; + +interface JsonNodeProps { + data: JsonValue; + name?: string; + depth: number; + initialExpandDepth: number; +} + +const JsonNode = memo( + ({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => { + const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); + + const getDataType = (value: JsonValue): string => { + if (Array.isArray(value)) return "array"; + if (value === null) return "null"; + return typeof value; + }; + + const dataType = getDataType(data); + + const typeStyleMap: Record = { + number: "text-blue-600", + boolean: "text-amber-600", + null: "text-purple-600", + undefined: "text-gray-600", + string: "text-green-600 break-all whitespace-pre-wrap", + default: "text-gray-700", + }; + + const renderCollapsible = (isArray: boolean) => { + const items = isArray + ? (data as JsonValue[]) + : Object.entries(data as Record); + const itemCount = items.length; + const isEmpty = itemCount === 0; + + const symbolMap = { + open: isArray ? "[" : "{", + close: isArray ? "]" : "}", + collapsed: isArray ? "[ ... ]" : "{ ... }", + empty: isArray ? "[]" : "{}", + }; + + if (isEmpty) { + return ( +
+ {name && ( + + {name}: + + )} + {symbolMap.empty} +
+ ); + } + + return ( +
+
setIsExpanded(!isExpanded)} + > + {name && ( + + {name}: + + )} + {isExpanded ? ( + + {symbolMap.open} + + ) : ( + <> + + {symbolMap.collapsed} + + + {itemCount} {itemCount === 1 ? "item" : "items"} + + + )} +
+ {isExpanded && ( + <> +
+ {isArray + ? (items as JsonValue[]).map((item, index) => ( +
+ +
+ )) + : (items as [string, JsonValue][]).map(([key, value]) => ( +
+ +
+ ))} +
+
+ {symbolMap.close} +
+ + )} +
+ ); + }; + + const renderString = (value: string) => { + const maxLength = 100; + const isTooLong = value.length > maxLength; + + if (!isTooLong) { + return ( +
+ {name && ( + + {name}: + + )} +
"{value}"
+
+ ); + } + + return ( +
+ {name && ( + + {name}: + + )} +
 setIsExpanded(!isExpanded)}
+            title={isExpanded ? "Click to collapse" : "Click to expand"}
+          >
+            {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
+          
+
+ ); + }; + + switch (dataType) { + case "object": + case "array": + return renderCollapsible(dataType === "array"); + case "string": + return renderString(data as string); + default: + return ( +
+ {name && ( + + {name}: + + )} + + {data === null ? "null" : String(data)} + +
+ ); + } + }, +); + +JsonNode.displayName = "JsonNode"; + +export default JsonView; diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index f88b16f..b42cf77 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Combobox } from "@/components/ui/combobox"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; + import { ListPromptsResult, PromptReference, @@ -13,6 +13,7 @@ import { AlertCircle } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; +import JsonView from "./JsonView"; export type Prompt = { name: string; @@ -151,11 +152,9 @@ const PromptsTab = ({ Get Prompt {promptContent && ( -