Skip to content

Commit df001c6

Browse files
authored
feat(tarko): one-click copy raw tool data (#1304)
1 parent d35602c commit df001c6

File tree

2 files changed

+118
-44
lines changed

2 files changed

+118
-44
lines changed

multimodal/tarko/agent-web-ui/src/common/components/JsonRenderer.tsx

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,10 @@ import { FiChevronRight, FiCopy, FiCheck, FiMaximize2, FiMinimize2 } from 'react
99
* Supports hierarchical tree structure with smooth animations.
1010
*/
1111

12-
interface JsonItemProps {
13-
label: string;
14-
value: any;
15-
level?: number;
16-
isRoot?: boolean;
17-
}
18-
19-
const JsonItem: React.FC<JsonItemProps> = ({ label, value, level = 0, isRoot = false }) => {
20-
const [isExpanded, setIsExpanded] = useState(level <= 1); // Expand root and first level
12+
// Shared copy hook
13+
const useCopy = () => {
2114
const [copied, setCopied] = useState(false);
22-
const [isStringExpanded, setIsStringExpanded] = useState(false);
23-
15+
2416
const handleCopy = useCallback(async (text: string) => {
2517
try {
2618
await navigator.clipboard.writeText(text);
@@ -30,6 +22,44 @@ const JsonItem: React.FC<JsonItemProps> = ({ label, value, level = 0, isRoot = f
3022
console.error('Failed to copy:', error);
3123
}
3224
}, []);
25+
26+
return { copied, handleCopy };
27+
};
28+
29+
// Shared copy button component
30+
const CopyButton: React.FC<{
31+
onCopy: () => void;
32+
copied: boolean;
33+
title?: string;
34+
size?: number;
35+
className?: string;
36+
}> = ({ onCopy, copied, title = 'Copy', size = 12, className = '' }) => (
37+
<motion.button
38+
whileHover={{ scale: 1.1 }}
39+
whileTap={{ scale: 0.95 }}
40+
onClick={onCopy}
41+
className={`p-1.5 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-all ${className}`}
42+
title={title}
43+
>
44+
{copied ? (
45+
<FiCheck size={size} className="text-green-500" />
46+
) : (
47+
<FiCopy size={size} className="text-gray-400" />
48+
)}
49+
</motion.button>
50+
);
51+
52+
interface JsonItemProps {
53+
label: string;
54+
value: any;
55+
level?: number;
56+
isRoot?: boolean;
57+
}
58+
59+
const JsonItem: React.FC<JsonItemProps> = ({ label, value, level = 0, isRoot = false }) => {
60+
const [isExpanded, setIsExpanded] = useState(level <= 1); // Expand root and first level
61+
const [isStringExpanded, setIsStringExpanded] = useState(false);
62+
const { copied, handleCopy } = useCopy();
3363

3464
const isObject = value && typeof value === 'object' && !Array.isArray(value);
3565
const isArray = Array.isArray(value);
@@ -94,19 +124,11 @@ const JsonItem: React.FC<JsonItemProps> = ({ label, value, level = 0, isRoot = f
94124
)}
95125
</motion.button>
96126
)}
97-
<motion.button
98-
whileHover={{ scale: 1.1 }}
99-
whileTap={{ scale: 0.95 }}
100-
onClick={() => handleCopy(displayValue)}
101-
className="p-1.5 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
127+
<CopyButton
128+
onCopy={() => handleCopy(displayValue)}
129+
copied={copied}
102130
title="Copy value"
103-
>
104-
{copied ? (
105-
<FiCheck size={12} className="text-green-500" />
106-
) : (
107-
<FiCopy size={12} className="text-gray-400" />
108-
)}
109-
</motion.button>
131+
/>
110132
</div>
111133
</div>
112134
</motion.div>
@@ -174,11 +196,18 @@ interface JsonRendererProps {
174196
emptyMessage?: string;
175197
}
176198

177-
export const JsonRenderer: React.FC<JsonRendererProps> = ({
178-
data,
179-
className = '',
180-
emptyMessage = 'No data available',
181-
}) => {
199+
export interface JsonRendererRef {
200+
copyAll: () => string;
201+
}
202+
203+
export const JsonRenderer = React.forwardRef<JsonRendererRef, JsonRendererProps>((
204+
{ data, className = '', emptyMessage = 'No data available' },
205+
ref
206+
) => {
207+
React.useImperativeHandle(ref, () => ({
208+
copyAll: () => JSON.stringify(data, null, 2)
209+
}), [data]);
210+
182211
if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
183212
return (
184213
<div className={`flex items-center justify-center py-8 ${className}`}>
@@ -208,4 +237,4 @@ export const JsonRenderer: React.FC<JsonRendererProps> = ({
208237
)}
209238
</div>
210239
);
211-
};
240+
});

multimodal/tarko/agent-web-ui/src/standalone/workspace/components/RawModeRenderer.tsx

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,58 @@
1-
import React from 'react';
1+
import React, { useRef, useState, useCallback } from 'react';
22
import { motion } from 'framer-motion';
3-
import { FiTerminal, FiClock, FiPlay, FiCheckCircle, FiXCircle } from 'react-icons/fi';
4-
import { JsonRenderer } from '@/common/components/JsonRenderer';
3+
import { FiTerminal, FiClock, FiPlay, FiCheckCircle, FiXCircle, FiCopy, FiCheck } from 'react-icons/fi';
4+
import { JsonRenderer, JsonRendererRef } from '@/common/components/JsonRenderer';
55
import { RawToolMapping } from '@/common/state/atoms/rawEvents';
66
import { formatTimestamp } from '@/common/utils/formatters';
77

88
interface RawModeRendererProps {
99
toolMapping: RawToolMapping;
1010
}
1111

12+
// Copy button component
13+
const CopyButton: React.FC<{
14+
jsonRef: React.RefObject<JsonRendererRef>;
15+
title: string;
16+
}> = ({ jsonRef, title }) => {
17+
const [copied, setCopied] = useState(false);
18+
19+
const handleCopy = useCallback(async () => {
20+
try {
21+
const jsonString = jsonRef.current?.copyAll();
22+
if (jsonString) {
23+
await navigator.clipboard.writeText(jsonString);
24+
setCopied(true);
25+
setTimeout(() => setCopied(false), 1500);
26+
}
27+
} catch (error) {
28+
console.error('Failed to copy JSON:', error);
29+
}
30+
}, [jsonRef]);
31+
32+
return (
33+
<motion.button
34+
whileHover={{ scale: 1.05 }}
35+
whileTap={{ scale: 0.95 }}
36+
onClick={handleCopy}
37+
className="p-1.5 rounded-md hover:bg-slate-200 dark:hover:bg-slate-700 transition-all opacity-0 group-hover:opacity-100"
38+
title={title}
39+
>
40+
{copied ? (
41+
<FiCheck size={12} className="text-green-500" />
42+
) : (
43+
<FiCopy size={12} className="text-slate-400" />
44+
)}
45+
</motion.button>
46+
);
47+
};
48+
1249
export const RawModeRenderer: React.FC<RawModeRendererProps> = ({ toolMapping }) => {
1350
const { toolCall, toolResult } = toolMapping;
51+
52+
// Refs for JsonRenderer components
53+
const parametersRef = useRef<JsonRendererRef>(null);
54+
const responseRef = useRef<JsonRendererRef>(null);
55+
const metadataRef = useRef<JsonRendererRef>(null);
1456

1557
return (
1658
<div className="space-y-3 mt-3">
@@ -50,12 +92,13 @@ export const RawModeRenderer: React.FC<RawModeRendererProps> = ({ toolMapping })
5092
</div>
5193
</div>
5294
{toolCall.arguments && (
53-
<div>
54-
<div className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
55-
Parameters
95+
<div className="group">
96+
<div className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2 flex items-center justify-between">
97+
<span>Parameters</span>
98+
<CopyButton jsonRef={parametersRef} title="Copy parameters JSON" />
5699
</div>
57100
<div className="bg-white/60 dark:bg-slate-800/60 rounded-lg p-3 border border-slate-200/40 dark:border-slate-700/40">
58-
<JsonRenderer data={toolCall.arguments} emptyMessage="No parameters provided" />
101+
<JsonRenderer ref={parametersRef} data={toolCall.arguments} emptyMessage="No parameters provided" />
59102
</div>
60103
</div>
61104
)}
@@ -134,21 +177,23 @@ export const RawModeRenderer: React.FC<RawModeRendererProps> = ({ toolMapping })
134177
</div>
135178
</div>
136179
)}
137-
<div>
138-
<div className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
139-
Response
180+
<div className="group">
181+
<div className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2 flex items-center justify-between">
182+
<span>Response</span>
183+
<CopyButton jsonRef={responseRef} title="Copy response JSON" />
140184
</div>
141185
<div className="bg-white/60 dark:bg-slate-800/60 rounded-lg p-3 border border-slate-200/40 dark:border-slate-700/40">
142-
<JsonRenderer data={toolResult.content} emptyMessage="No response data" />
186+
<JsonRenderer ref={responseRef} data={toolResult.content} emptyMessage="No response data" />
143187
</div>
144188
</div>
145189
{toolResult._extra && (
146-
<div>
147-
<div className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
148-
Metadata
190+
<div className="group">
191+
<div className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2 flex items-center justify-between">
192+
<span>Metadata</span>
193+
<CopyButton jsonRef={metadataRef} title="Copy metadata JSON" />
149194
</div>
150195
<div className="bg-white/60 dark:bg-slate-800/60 rounded-lg p-3 border border-slate-200/40 dark:border-slate-700/40">
151-
<JsonRenderer data={toolResult._extra} emptyMessage="No metadata" />
196+
<JsonRenderer ref={metadataRef} data={toolResult._extra} emptyMessage="No metadata" />
152197
</div>
153198
</div>
154199
)}

0 commit comments

Comments
 (0)