Use copy button insde JSON view component
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Copy } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import JsonView from "./JsonView";
|
import JsonView from "./JsonView";
|
||||||
|
|
||||||
@@ -25,10 +24,6 @@ const HistoryAndNotifications = ({
|
|||||||
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
|
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = (text: string) => {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card overflow-hidden flex h-full">
|
<div className="bg-card overflow-hidden flex h-full">
|
||||||
<div className="flex-1 overflow-y-auto p-4 border-r">
|
<div className="flex-1 overflow-y-auto p-4 border-r">
|
||||||
@@ -68,16 +63,12 @@ const HistoryAndNotifications = ({
|
|||||||
<span className="font-semibold text-blue-600">
|
<span className="font-semibold text-blue-600">
|
||||||
Request:
|
Request:
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
onClick={() => copyToClipboard(request.request)}
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
<Copy size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="bg-background p-2 rounded">
|
|
||||||
<JsonView data={request.request} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<JsonView
|
||||||
|
data={request.request}
|
||||||
|
className="bg-background"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{request.response && (
|
{request.response && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -85,16 +76,11 @@ const HistoryAndNotifications = ({
|
|||||||
<span className="font-semibold text-green-600">
|
<span className="font-semibold text-green-600">
|
||||||
Response:
|
Response:
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
onClick={() => copyToClipboard(request.response!)}
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
<Copy size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="bg-background p-2 rounded">
|
|
||||||
<JsonView data={request.response} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<JsonView
|
||||||
|
data={request.response}
|
||||||
|
className="bg-background"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -134,21 +120,12 @@ const HistoryAndNotifications = ({
|
|||||||
<span className="font-semibold text-purple-600">
|
<span className="font-semibold text-purple-600">
|
||||||
Details:
|
Details:
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
copyToClipboard(JSON.stringify(notification))
|
|
||||||
}
|
|
||||||
className="text-blue-500 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
<Copy size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-background p-2 rounded">
|
|
||||||
<JsonView
|
<JsonView
|
||||||
data={JSON.stringify(notification, null, 2)}
|
data={JSON.stringify(notification, null, 2)}
|
||||||
|
className="bg-background"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { useState, memo } from "react";
|
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
||||||
import { JsonValue } from "./DynamicJsonForm";
|
import { JsonValue } from "./DynamicJsonForm";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { Copy, CheckCheck } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface JsonViewProps {
|
interface JsonViewProps {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
name?: string;
|
name?: string;
|
||||||
initialExpandDepth?: number;
|
initialExpandDepth?: number;
|
||||||
|
className?: string;
|
||||||
|
withCopyButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
||||||
@@ -24,15 +29,67 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const JsonView = memo(
|
const JsonView = memo(
|
||||||
({ data, name, initialExpandDepth = 3 }: JsonViewProps) => {
|
({
|
||||||
const normalizedData =
|
data,
|
||||||
typeof data === "string"
|
name,
|
||||||
|
initialExpandDepth = 3,
|
||||||
|
className,
|
||||||
|
withCopyButton = true,
|
||||||
|
}: JsonViewProps) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
if (copied) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
const normalizedData = useMemo(() => {
|
||||||
|
return typeof data === "string"
|
||||||
? tryParseJson(data).success
|
? tryParseJson(data).success
|
||||||
? tryParseJson(data).data
|
? tryParseJson(data).data
|
||||||
: data
|
: data
|
||||||
: data;
|
: data;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(() => {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(normalizedData, null, 2));
|
||||||
|
setCopied(true);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [toast, normalizedData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={clsx("p-4 border rounded relative", className)}>
|
||||||
|
{withCopyButton && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-4 text-foreground" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<div className="font-mono text-sm transition-all duration-300">
|
<div className="font-mono text-sm transition-all duration-300">
|
||||||
<JsonNode
|
<JsonNode
|
||||||
data={normalizedData as JsonValue}
|
data={normalizedData as JsonValue}
|
||||||
@@ -41,6 +98,7 @@ const JsonView = memo(
|
|||||||
initialExpandDepth={initialExpandDepth}
|
initialExpandDepth={initialExpandDepth}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -152,9 +152,7 @@ const PromptsTab = ({
|
|||||||
Get Prompt
|
Get Prompt
|
||||||
</Button>
|
</Button>
|
||||||
{promptContent && (
|
{promptContent && (
|
||||||
<div className="p-4 border rounded">
|
<JsonView data={promptContent} withCopyButton={false} />
|
||||||
<JsonView data={promptContent} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -215,9 +215,10 @@ const ResourcesTab = ({
|
|||||||
<AlertDescription>{error}</AlertDescription>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : selectedResource ? (
|
) : selectedResource ? (
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100">
|
<JsonView
|
||||||
<JsonView data={resourceContent} />
|
data={resourceContent}
|
||||||
</div>
|
className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
) : selectedTemplate ? (
|
) : selectedTemplate ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
|||||||
<h3 className="text-lg font-semibold">Recent Requests</h3>
|
<h3 className="text-lg font-semibold">Recent Requests</h3>
|
||||||
{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">
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
|
<JsonView
|
||||||
<JsonView data={JSON.stringify(request.request)} />
|
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 rounded"
|
||||||
</div>
|
data={JSON.stringify(request.request)}
|
||||||
|
/>
|
||||||
|
|
||||||
<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>
|
||||||
<Button variant="outline" onClick={() => onReject(request.id)}>
|
<Button variant="outline" onClick={() => onReject(request.id)}>
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import {
|
|||||||
ListToolsResult,
|
ListToolsResult,
|
||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Copy, Send, CheckCheck } from "lucide-react";
|
import { Send } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ListPane from "./ListPane";
|
import ListPane from "./ListPane";
|
||||||
import JsonView from "./JsonView";
|
import JsonView from "./JsonView";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
|
|
||||||
const ToolsTab = ({
|
const ToolsTab = ({
|
||||||
tools,
|
tools,
|
||||||
@@ -39,30 +38,11 @@ const ToolsTab = ({
|
|||||||
nextCursor: ListToolsResult["nextCursor"];
|
nextCursor: ListToolsResult["nextCursor"];
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { toast } = useToast();
|
|
||||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setParams({});
|
setParams({});
|
||||||
}, [selectedTool]);
|
}, [selectedTool]);
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
|
||||||
try {
|
|
||||||
navigator.clipboard.writeText(JSON.stringify(toolResult));
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setCopied(false);
|
|
||||||
}, 500);
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [toast, toolResult]);
|
|
||||||
|
|
||||||
const renderToolResult = () => {
|
const renderToolResult = () => {
|
||||||
if (!toolResult) return null;
|
if (!toolResult) return null;
|
||||||
|
|
||||||
@@ -72,15 +52,10 @@ const ToolsTab = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
||||||
<div className="p-4 border rounded relative">
|
|
||||||
<Copy className="size-4 text-primary" />
|
|
||||||
<JsonView data={toolResult} />
|
<JsonView data={toolResult} />
|
||||||
</div>
|
|
||||||
<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) => (
|
||||||
<div key={idx} className="p-4 border rounded">
|
<JsonView data={error} key={idx} />
|
||||||
<JsonView data={error} />
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -95,23 +70,7 @@ const ToolsTab = ({
|
|||||||
</h4>
|
</h4>
|
||||||
{structuredResult.content.map((item, index) => (
|
{structuredResult.content.map((item, index) => (
|
||||||
<div key={index} className="mb-2">
|
<div key={index} className="mb-2">
|
||||||
{item.type === "text" && (
|
{item.type === "text" && <JsonView data={item.text} />}
|
||||||
<div className="p-4 border rounded relative">
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="absolute top-2 right-2"
|
|
||||||
onClick={handleCopy}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<Copy className="size-4 text-foreground" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<JsonView data={item.text} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.type === "image" && (
|
{item.type === "image" && (
|
||||||
<img
|
<img
|
||||||
src={`data:${item.mimeType};base64,${item.data}`}
|
src={`data:${item.mimeType};base64,${item.data}`}
|
||||||
@@ -129,9 +88,7 @@ const ToolsTab = ({
|
|||||||
<p>Your browser does not support audio playback</p>
|
<p>Your browser does not support audio playback</p>
|
||||||
</audio>
|
</audio>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-4 border rounded">
|
|
||||||
<JsonView data={item.resource} />
|
<JsonView data={item.resource} />
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -141,9 +98,8 @@ const ToolsTab = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
||||||
<div className="p-4 border rounded">
|
|
||||||
<JsonView data={toolResult.toolResult} />
|
<JsonView data={toolResult.toolResult} />
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user