Skip to content

Commit c964ff5

Browse files
Use copy button insde JSON view component
1 parent c9ee22b commit c964ff5

File tree

6 files changed

+100
-108
lines changed

6 files changed

+100
-108
lines changed

client/src/components/History.tsx

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
2-
import { Copy } from "lucide-react";
32
import { useState } from "react";
43
import JsonView from "./JsonView";
54

@@ -25,10 +24,6 @@ const HistoryAndNotifications = ({
2524
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
2625
};
2726

28-
const copyToClipboard = (text: string) => {
29-
navigator.clipboard.writeText(text);
30-
};
31-
3227
return (
3328
<div className="bg-card overflow-hidden flex h-full">
3429
<div className="flex-1 overflow-y-auto p-4 border-r">
@@ -68,33 +63,24 @@ const HistoryAndNotifications = ({
6863
<span className="font-semibold text-blue-600">
6964
Request:
7065
</span>
71-
<button
72-
onClick={() => copyToClipboard(request.request)}
73-
className="text-blue-500 hover:text-blue-700"
74-
>
75-
<Copy size={16} />
76-
</button>
77-
</div>
78-
<div className="bg-background p-2 rounded">
79-
<JsonView data={request.request} />
8066
</div>
67+
68+
<JsonView
69+
data={request.request}
70+
className="bg-background"
71+
/>
8172
</div>
8273
{request.response && (
8374
<div className="mt-2">
8475
<div className="flex justify-between items-center mb-1">
8576
<span className="font-semibold text-green-600">
8677
Response:
8778
</span>
88-
<button
89-
onClick={() => copyToClipboard(request.response!)}
90-
className="text-blue-500 hover:text-blue-700"
91-
>
92-
<Copy size={16} />
93-
</button>
94-
</div>
95-
<div className="bg-background p-2 rounded">
96-
<JsonView data={request.response} />
9779
</div>
80+
<JsonView
81+
data={request.response}
82+
className="bg-background"
83+
/>
9884
</div>
9985
)}
10086
</>
@@ -134,20 +120,11 @@ const HistoryAndNotifications = ({
134120
<span className="font-semibold text-purple-600">
135121
Details:
136122
</span>
137-
<button
138-
onClick={() =>
139-
copyToClipboard(JSON.stringify(notification))
140-
}
141-
className="text-blue-500 hover:text-blue-700"
142-
>
143-
<Copy size={16} />
144-
</button>
145-
</div>
146-
<div className="bg-background p-2 rounded">
147-
<JsonView
148-
data={JSON.stringify(notification, null, 2)}
149-
/>
150123
</div>
124+
<JsonView
125+
data={JSON.stringify(notification, null, 2)}
126+
className="bg-background"
127+
/>
151128
</div>
152129
)}
153130
</li>

client/src/components/JsonView.tsx

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { useState, memo } from "react";
1+
import { useState, memo, useMemo, useCallback, useEffect } from "react";
22
import { JsonValue } from "./DynamicJsonForm";
33
import clsx from "clsx";
4+
import { Copy, CheckCheck } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
import { useToast } from "@/hooks/use-toast";
47

58
interface JsonViewProps {
69
data: unknown;
710
name?: string;
811
initialExpandDepth?: number;
12+
className?: string;
13+
withCopyButton?: boolean;
914
}
1015

1116
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
@@ -24,22 +29,75 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } {
2429
}
2530

2631
const JsonView = memo(
27-
({ data, name, initialExpandDepth = 3 }: JsonViewProps) => {
28-
const normalizedData =
29-
typeof data === "string"
32+
({
33+
data,
34+
name,
35+
initialExpandDepth = 3,
36+
className,
37+
withCopyButton = true,
38+
}: JsonViewProps) => {
39+
const { toast } = useToast();
40+
const [copied, setCopied] = useState(false);
41+
42+
useEffect(() => {
43+
let timeoutId: NodeJS.Timeout;
44+
if (copied) {
45+
timeoutId = setTimeout(() => {
46+
setCopied(false);
47+
}, 500);
48+
}
49+
return () => {
50+
if (timeoutId) {
51+
clearTimeout(timeoutId);
52+
}
53+
};
54+
}, [copied]);
55+
56+
const normalizedData = useMemo(() => {
57+
return typeof data === "string"
3058
? tryParseJson(data).success
3159
? tryParseJson(data).data
3260
: data
3361
: data;
62+
}, [data]);
63+
64+
const handleCopy = useCallback(() => {
65+
try {
66+
navigator.clipboard.writeText(JSON.stringify(normalizedData, null, 2));
67+
setCopied(true);
68+
} catch (error) {
69+
toast({
70+
title: "Error",
71+
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
72+
variant: "destructive",
73+
});
74+
}
75+
}, [toast, normalizedData]);
3476

3577
return (
36-
<div className="font-mono text-sm transition-all duration-300">
37-
<JsonNode
38-
data={normalizedData as JsonValue}
39-
name={name}
40-
depth={0}
41-
initialExpandDepth={initialExpandDepth}
42-
/>
78+
<div className={clsx("p-4 border rounded relative", className)}>
79+
{withCopyButton && (
80+
<Button
81+
size="icon"
82+
variant="ghost"
83+
className="absolute top-2 right-2"
84+
onClick={handleCopy}
85+
>
86+
{copied ? (
87+
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
88+
) : (
89+
<Copy className="size-4 text-foreground" />
90+
)}
91+
</Button>
92+
)}
93+
<div className="font-mono text-sm transition-all duration-300">
94+
<JsonNode
95+
data={normalizedData as JsonValue}
96+
name={name}
97+
depth={0}
98+
initialExpandDepth={initialExpandDepth}
99+
/>
100+
</div>
43101
</div>
44102
);
45103
},

client/src/components/PromptsTab.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,7 @@ const PromptsTab = ({
152152
Get Prompt
153153
</Button>
154154
{promptContent && (
155-
<div className="p-4 border rounded">
156-
<JsonView data={promptContent} />
157-
</div>
155+
<JsonView data={promptContent} withCopyButton={false} />
158156
)}
159157
</div>
160158
) : (

client/src/components/ResourcesTab.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,10 @@ const ResourcesTab = ({
215215
<AlertDescription>{error}</AlertDescription>
216216
</Alert>
217217
) : selectedResource ? (
218-
<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">
219-
<JsonView data={resourceContent} />
220-
</div>
218+
<JsonView
219+
data={resourceContent}
220+
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"
221+
/>
221222
) : selectedTemplate ? (
222223
<div className="space-y-4">
223224
<p className="text-sm text-gray-600">

client/src/components/SamplingTab.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
4444
<h3 className="text-lg font-semibold">Recent Requests</h3>
4545
{pendingRequests.map((request) => (
4646
<div key={request.id} className="p-4 border rounded-lg space-y-4">
47-
<div className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
48-
<JsonView data={JSON.stringify(request.request)} />
49-
</div>
47+
<JsonView
48+
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 rounded"
49+
data={JSON.stringify(request.request)}
50+
/>
51+
5052
<div className="flex space-x-2">
5153
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
5254
<Button variant="outline" onClick={() => onReject(request.id)}>

client/src/components/ToolsTab.tsx

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import {
1313
ListToolsResult,
1414
Tool,
1515
} from "@modelcontextprotocol/sdk/types.js";
16-
import { Copy, Send, CheckCheck } from "lucide-react";
17-
import { useCallback, useEffect, useState } from "react";
16+
import { Send } from "lucide-react";
17+
import { useEffect, useState } from "react";
1818
import ListPane from "./ListPane";
1919
import JsonView from "./JsonView";
20-
import { useToast } from "@/hooks/use-toast";
2120

2221
const ToolsTab = ({
2322
tools,
@@ -39,30 +38,11 @@ const ToolsTab = ({
3938
nextCursor: ListToolsResult["nextCursor"];
4039
error: string | null;
4140
}) => {
42-
const { toast } = useToast();
4341
const [params, setParams] = useState<Record<string, unknown>>({});
4442
useEffect(() => {
4543
setParams({});
4644
}, [selectedTool]);
4745

48-
const [copied, setCopied] = useState(false);
49-
50-
const handleCopy = useCallback(() => {
51-
try {
52-
navigator.clipboard.writeText(JSON.stringify(toolResult));
53-
setCopied(true);
54-
setTimeout(() => {
55-
setCopied(false);
56-
}, 500);
57-
} catch (error) {
58-
toast({
59-
title: "Error",
60-
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
61-
variant: "destructive",
62-
});
63-
}
64-
}, [toast, toolResult]);
65-
6646
const renderToolResult = () => {
6747
if (!toolResult) return null;
6848

@@ -72,15 +52,10 @@ const ToolsTab = ({
7252
return (
7353
<>
7454
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
75-
<div className="p-4 border rounded relative">
76-
<Copy className="size-4 text-primary" />
77-
<JsonView data={toolResult} />
78-
</div>
55+
<JsonView data={toolResult} />
7956
<h4 className="font-semibold mb-2">Errors:</h4>
8057
{parsedResult.error.errors.map((error, idx) => (
81-
<div key={idx} className="p-4 border rounded">
82-
<JsonView data={error} />
83-
</div>
58+
<JsonView data={error} key={idx} />
8459
))}
8560
</>
8661
);
@@ -95,23 +70,7 @@ const ToolsTab = ({
9570
</h4>
9671
{structuredResult.content.map((item, index) => (
9772
<div key={index} className="mb-2">
98-
{item.type === "text" && (
99-
<div className="p-4 border rounded relative">
100-
<Button
101-
size="icon"
102-
variant="ghost"
103-
className="absolute top-2 right-2"
104-
onClick={handleCopy}
105-
>
106-
{copied ? (
107-
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
108-
) : (
109-
<Copy className="size-4 text-foreground" />
110-
)}
111-
</Button>
112-
<JsonView data={item.text} />
113-
</div>
114-
)}
73+
{item.type === "text" && <JsonView data={item.text} />}
11574
{item.type === "image" && (
11675
<img
11776
src={`data:${item.mimeType};base64,${item.data}`}
@@ -129,9 +88,7 @@ const ToolsTab = ({
12988
<p>Your browser does not support audio playback</p>
13089
</audio>
13190
) : (
132-
<div className="p-4 border rounded">
133-
<JsonView data={item.resource} />
134-
</div>
91+
<JsonView data={item.resource} />
13592
))}
13693
</div>
13794
))}
@@ -141,9 +98,8 @@ const ToolsTab = ({
14198
return (
14299
<>
143100
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
144-
<div className="p-4 border rounded">
145-
<JsonView data={toolResult.toolResult} />
146-
</div>
101+
102+
<JsonView data={toolResult.toolResult} />
147103
</>
148104
);
149105
}

0 commit comments

Comments
 (0)