feat: json view component
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Copy } from "lucide-react";
|
import { Copy } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import JsonView from "./JsonView";
|
||||||
|
|
||||||
const HistoryAndNotifications = ({
|
const HistoryAndNotifications = ({
|
||||||
requestHistory,
|
requestHistory,
|
||||||
@@ -75,7 +76,7 @@ const HistoryAndNotifications = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
|
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
|
||||||
{JSON.stringify(JSON.parse(request.request), null, 2)}
|
<JsonView data={request.request} />
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
{request.response && (
|
{request.response && (
|
||||||
@@ -92,11 +93,7 @@ const HistoryAndNotifications = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
|
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
|
||||||
{JSON.stringify(
|
<JsonView data={request.response} />
|
||||||
JSON.parse(request.response),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -147,7 +144,9 @@ const HistoryAndNotifications = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
|
<pre className="whitespace-pre-wrap break-words bg-background p-2 rounded">
|
||||||
{JSON.stringify(notification, null, 2)}
|
<JsonView
|
||||||
|
data={JSON.stringify(notification, null, 2)}
|
||||||
|
/>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
210
client/src/components/JsonView.tsx
Normal file
210
client/src/components/JsonView.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { useState, memo } from "react";
|
||||||
|
import { JsonValue } from "./DynamicJsonForm";
|
||||||
|
|
||||||
|
interface JsonViewProps {
|
||||||
|
data: JsonValue;
|
||||||
|
name?: string;
|
||||||
|
initialExpandDepth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
||||||
|
const trimmed = str.trim();
|
||||||
|
if (
|
||||||
|
!(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
|
||||||
|
!(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
||||||
|
) {
|
||||||
|
return { success: false, data: str };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return { success: true, data: JSON.parse(str) };
|
||||||
|
} catch {
|
||||||
|
return { success: false, data: str };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonView = memo(
|
||||||
|
({ data, name, initialExpandDepth = 2 }: JsonViewProps) => {
|
||||||
|
const normalizedData =
|
||||||
|
typeof data === "string"
|
||||||
|
? tryParseJson(data).success
|
||||||
|
? tryParseJson(data).data
|
||||||
|
: data
|
||||||
|
: data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="font-mono text-sm transition-all duration-300">
|
||||||
|
<JsonNode
|
||||||
|
data={normalizedData}
|
||||||
|
name={name}
|
||||||
|
depth={0}
|
||||||
|
initialExpandDepth={initialExpandDepth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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<string, string> = {
|
||||||
|
number: "text-blue-600",
|
||||||
|
boolean: "text-amber-600",
|
||||||
|
null: "text-purple-600",
|
||||||
|
undefined: "text-gray-600",
|
||||||
|
string: "text-green-600",
|
||||||
|
default: "text-gray-700",
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCollapsible = (isArray: boolean) => {
|
||||||
|
const items = isArray
|
||||||
|
? (data as JsonValue[])
|
||||||
|
: Object.entries(data as Record<string, JsonValue>);
|
||||||
|
const itemCount = items.length;
|
||||||
|
const isEmpty = itemCount === 0;
|
||||||
|
|
||||||
|
const symbolMap = {
|
||||||
|
open: isArray ? "[" : "{",
|
||||||
|
close: isArray ? "]" : "}",
|
||||||
|
collapsed: isArray ? "[ ... ]" : "{ ... }",
|
||||||
|
empty: isArray ? "[]" : "{}",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{name && <span className="mr-1 text-gray-400">{name}:</span>}
|
||||||
|
<span className="text-gray-500">{symbolMap.empty}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div
|
||||||
|
className="flex items-center mr-1 rounded cursor-pointer group hover:bg-gray-800/20"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
|
{name && (
|
||||||
|
<span className="mr-1 text-gray-400 group-hover:text-gray-100">
|
||||||
|
{name}:
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isExpanded ? (
|
||||||
|
<span className="text-gray-400 group-hover:text-gray-100">
|
||||||
|
{symbolMap.open}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-gray-600 group-hover:text-gray-100">
|
||||||
|
{symbolMap.collapsed}
|
||||||
|
</span>
|
||||||
|
<span className="ml-1 text-gray-700 group-hover:text-gray-100">
|
||||||
|
{itemCount} {itemCount === 1 ? "item" : "items"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isExpanded && (
|
||||||
|
<>
|
||||||
|
<div className="pl-2 ml-4 border-l border-gray-800">
|
||||||
|
{isArray
|
||||||
|
? (items as JsonValue[]).map((item, index) => (
|
||||||
|
<div key={index} className="my-1">
|
||||||
|
<JsonNode
|
||||||
|
data={item}
|
||||||
|
name={`${index}`}
|
||||||
|
depth={depth + 1}
|
||||||
|
initialExpandDepth={initialExpandDepth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: (items as [string, JsonValue][]).map(([key, value]) => (
|
||||||
|
<div key={key} className="my-1">
|
||||||
|
<JsonNode
|
||||||
|
data={value}
|
||||||
|
name={key}
|
||||||
|
depth={depth + 1}
|
||||||
|
initialExpandDepth={initialExpandDepth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400">{symbolMap.close}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderString = (value: string) => {
|
||||||
|
const maxLength = 100;
|
||||||
|
const isTooLong = value.length > maxLength;
|
||||||
|
|
||||||
|
if (!isTooLong) {
|
||||||
|
return (
|
||||||
|
<div className="flex mr-1 rounded hover:bg-gray-800/20">
|
||||||
|
{name && <span className="mr-1 text-gray-400">{name}:</span>}
|
||||||
|
<span className={typeStyleMap.string}>"{value}"</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex mr-1 rounded group hover:bg-gray-800/20">
|
||||||
|
{name && (
|
||||||
|
<span className="mr-1 text-gray-400 group-hover:text-gray-100">
|
||||||
|
{name}:
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`${typeStyleMap.string} cursor-pointer group-hover:text-green-500`}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
title={isExpanded ? "클릭하여 축소" : "클릭하여 전체 보기"}
|
||||||
|
>
|
||||||
|
{isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (dataType) {
|
||||||
|
case "object":
|
||||||
|
case "array":
|
||||||
|
return renderCollapsible(dataType === "array");
|
||||||
|
case "string":
|
||||||
|
return renderString(data as string);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="flex items-center mr-1 rounded hover:bg-gray-800/20">
|
||||||
|
{name && <span className="mr-1 text-gray-400">{name}:</span>}
|
||||||
|
<span className={typeStyleMap[dataType] || typeStyleMap.default}>
|
||||||
|
{data === null ? "null" : String(data)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
JsonNode.displayName = "JsonNode";
|
||||||
|
|
||||||
|
export default JsonView;
|
||||||
@@ -15,6 +15,7 @@ import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
|
|||||||
import ListPane from "./ListPane";
|
import ListPane from "./ListPane";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useCompletionState } from "@/lib/hooks/useCompletionState";
|
import { useCompletionState } from "@/lib/hooks/useCompletionState";
|
||||||
|
import JsonView from "./JsonView";
|
||||||
|
|
||||||
const ResourcesTab = ({
|
const ResourcesTab = ({
|
||||||
resources,
|
resources,
|
||||||
@@ -215,7 +216,7 @@ const ResourcesTab = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : selectedResource ? (
|
) : selectedResource ? (
|
||||||
<pre className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
|
<pre className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
|
||||||
{resourceContent}
|
<JsonView data={resourceContent} />
|
||||||
</pre>
|
</pre>
|
||||||
) : selectedTemplate ? (
|
) : selectedTemplate ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CreateMessageRequest,
|
CreateMessageRequest,
|
||||||
CreateMessageResult,
|
CreateMessageResult,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import JsonView from "./JsonView";
|
||||||
|
|
||||||
export type PendingRequest = {
|
export type PendingRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -44,7 +45,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
|||||||
{pendingRequests.map((request) => (
|
{pendingRequests.map((request) => (
|
||||||
<div key={request.id} className="p-4 border rounded-lg space-y-4">
|
<div key={request.id} className="p-4 border rounded-lg space-y-4">
|
||||||
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
|
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
|
||||||
{JSON.stringify(request.request, null, 2)}
|
<JsonView data={JSON.stringify(request.request)} />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
|
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { AlertCircle, Send } from "lucide-react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ListPane from "./ListPane";
|
import ListPane from "./ListPane";
|
||||||
import { escapeUnicode } from "@/utils/escapeUnicode";
|
import { escapeUnicode } from "@/utils/escapeUnicode";
|
||||||
|
import JsonView from "./JsonView";
|
||||||
|
|
||||||
const ToolsTab = ({
|
const ToolsTab = ({
|
||||||
tools,
|
tools,
|
||||||
@@ -54,7 +55,7 @@ const ToolsTab = ({
|
|||||||
<>
|
<>
|
||||||
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
||||||
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
|
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
|
||||||
{escapeUnicode(toolResult)}
|
<JsonView data={escapeUnicode(toolResult)} />
|
||||||
</pre>
|
</pre>
|
||||||
<h4 className="font-semibold mb-2">Errors:</h4>
|
<h4 className="font-semibold mb-2">Errors:</h4>
|
||||||
{parsedResult.error.errors.map((error, idx) => (
|
{parsedResult.error.errors.map((error, idx) => (
|
||||||
@@ -62,7 +63,7 @@ const ToolsTab = ({
|
|||||||
key={idx}
|
key={idx}
|
||||||
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64"
|
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64"
|
||||||
>
|
>
|
||||||
{escapeUnicode(error)}
|
<JsonView data={escapeUnicode(error)} />
|
||||||
</pre>
|
</pre>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -80,7 +81,7 @@ const ToolsTab = ({
|
|||||||
<div key={index} className="mb-2">
|
<div key={index} className="mb-2">
|
||||||
{item.type === "text" && (
|
{item.type === "text" && (
|
||||||
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
|
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
|
||||||
{item.text}
|
<JsonView data={item.text} />
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
{item.type === "image" && (
|
{item.type === "image" && (
|
||||||
@@ -101,7 +102,7 @@ const ToolsTab = ({
|
|||||||
</audio>
|
</audio>
|
||||||
) : (
|
) : (
|
||||||
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
|
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 whitespace-pre-wrap break-words p-4 rounded text-sm overflow-auto max-h-64">
|
||||||
{escapeUnicode(item.resource)}
|
<JsonView data={escapeUnicode(item.resource)} />
|
||||||
</pre>
|
</pre>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +114,7 @@ const ToolsTab = ({
|
|||||||
<>
|
<>
|
||||||
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
||||||
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
|
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
|
||||||
{escapeUnicode(toolResult.toolResult)}
|
<JsonView data={escapeUnicode(toolResult.toolResult)} />
|
||||||
</pre>
|
</pre>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user