@tonyclaw/llm-inspector 1.14.7 → 1.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-Dv-dj1xH.js +105 -0
- package/.output/public/assets/index-bqeypwJB.css +1 -0
- package/.output/public/assets/{main-BV7uNIIz.js → main-C8OUJKbz.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +87 -79
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_ssr/{index-BvHLASu8.mjs → index-_9xcAkkw.mjs} +861 -608
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-lUOA8pi6.mjs → router-CmanwZJc.mjs} +45 -14
- package/.output/server/{_tanstack-start-manifest_v-XNH7fVPN.mjs → _tanstack-start-manifest_v-BVIiyDeJ.mjs} +1 -1
- package/.output/server/index.mjs +23 -23
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +137 -146
- package/src/components/providers/ProviderCard.tsx +79 -26
- package/src/components/providers/ProviderForm.tsx +37 -22
- package/src/components/providers/ProvidersPanel.tsx +79 -47
- package/src/components/providers/SettingsDialog.tsx +25 -15
- package/src/components/proxy-viewer/ConversationGroup.tsx +74 -11
- package/src/components/proxy-viewer/ConversationHeader.tsx +63 -2
- package/src/components/proxy-viewer/LogEntry.tsx +184 -54
- package/src/components/proxy-viewer/LogEntryHeader.tsx +148 -143
- package/src/components/proxy-viewer/ReplayDialog.tsx +16 -6
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +24 -16
- package/src/components/proxy-viewer/ThreadConnector.tsx +93 -0
- package/src/components/proxy-viewer/index.ts +2 -1
- package/src/lib/stopReason.ts +57 -0
- package/src/proxy/formats/anthropic/handler.ts +2 -5
- package/src/proxy/formats/openai/handler.ts +33 -7
- package/src/proxy/formats/openai/schemas.ts +1 -0
- package/src/proxy/formats/openai/stream.ts +24 -0
- package/src/proxy/handler.ts +8 -2
- package/src/proxy/schemas.ts +6 -3
- package/.output/public/assets/index-Cmi8TfeU.js +0 -105
- package/.output/public/assets/index-DXUNTCVh.css +0 -1
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ChevronDown,
|
|
3
|
+
ChevronRight,
|
|
4
|
+
Clock,
|
|
5
|
+
GitBranch,
|
|
6
|
+
Loader2,
|
|
7
|
+
MessageSquare,
|
|
8
|
+
User,
|
|
9
|
+
Zap,
|
|
10
|
+
} from "lucide-react";
|
|
2
11
|
import type { JSX } from "react";
|
|
3
12
|
import { cn, formatTokens } from "../../lib/utils";
|
|
4
13
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
@@ -10,6 +19,8 @@ const API_FORMAT_LABELS: Record<"anthropic" | "openai" | "unknown", string> = {
|
|
|
10
19
|
unknown: "Unknown",
|
|
11
20
|
};
|
|
12
21
|
|
|
22
|
+
export type ViewMode = "flat" | "thread";
|
|
23
|
+
|
|
13
24
|
export type ConversationHeaderProps = {
|
|
14
25
|
conversationId: string;
|
|
15
26
|
startTime: string;
|
|
@@ -23,6 +34,15 @@ export type ConversationHeaderProps = {
|
|
|
23
34
|
/** Hide the API format badge on the header (used when the group contains
|
|
24
35
|
* mixed formats — the per-log badges are shown instead). */
|
|
25
36
|
hideApiFormat?: boolean;
|
|
37
|
+
/** When true and the group is collapsed, show a spinner instead of the
|
|
38
|
+
* expand chevron to indicate an in-flight request inside the group. */
|
|
39
|
+
isLoading?: boolean;
|
|
40
|
+
/** Current display mode for this group (flat cards or threaded timeline). */
|
|
41
|
+
viewMode?: ViewMode;
|
|
42
|
+
/** Toggle between flat and thread display modes for this group. */
|
|
43
|
+
onToggleViewMode?: () => void;
|
|
44
|
+
/** User-Agent string from the first log in the group. */
|
|
45
|
+
userAgent?: string | null;
|
|
26
46
|
};
|
|
27
47
|
|
|
28
48
|
function formatTimestamp(iso: string): string {
|
|
@@ -41,6 +61,10 @@ export function ConversationHeader({
|
|
|
41
61
|
expanded,
|
|
42
62
|
onToggle,
|
|
43
63
|
hideApiFormat = false,
|
|
64
|
+
isLoading = false,
|
|
65
|
+
viewMode,
|
|
66
|
+
onToggleViewMode,
|
|
67
|
+
userAgent,
|
|
44
68
|
}: ConversationHeaderProps): JSX.Element {
|
|
45
69
|
return (
|
|
46
70
|
<div
|
|
@@ -60,13 +84,39 @@ export function ConversationHeader({
|
|
|
60
84
|
}
|
|
61
85
|
}}
|
|
62
86
|
>
|
|
63
|
-
{/* Expand chevron */}
|
|
87
|
+
{/* Expand chevron — shows spinner when collapsed and group has pending logs */}
|
|
64
88
|
{expanded ? (
|
|
65
89
|
<ChevronDown className="size-4 text-muted-foreground shrink-0" />
|
|
90
|
+
) : isLoading ? (
|
|
91
|
+
<Loader2 className="size-4 animate-spin text-muted-foreground shrink-0" />
|
|
66
92
|
) : (
|
|
67
93
|
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
68
94
|
)}
|
|
69
95
|
|
|
96
|
+
{/* Thread/flat view toggle — only shown when expanded */}
|
|
97
|
+
{expanded && onToggleViewMode !== undefined && (
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={(e) => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
onToggleViewMode();
|
|
103
|
+
}}
|
|
104
|
+
className={cn(
|
|
105
|
+
"px-1.5 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 cursor-pointer",
|
|
106
|
+
viewMode === "thread"
|
|
107
|
+
? "bg-amber-500/15 text-amber-400 border border-amber-500/30"
|
|
108
|
+
: "bg-muted text-muted-foreground border border-border hover:text-foreground",
|
|
109
|
+
)}
|
|
110
|
+
title={
|
|
111
|
+
viewMode === "thread"
|
|
112
|
+
? "Thread view — click for flat view"
|
|
113
|
+
: "Flat view — click for thread view"
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<GitBranch className="size-3" />
|
|
117
|
+
</button>
|
|
118
|
+
)}
|
|
119
|
+
|
|
70
120
|
{/* Conversation ID */}
|
|
71
121
|
<span
|
|
72
122
|
className="text-purple-400/90 font-mono text-xs font-semibold shrink-0"
|
|
@@ -77,6 +127,17 @@ export function ConversationHeader({
|
|
|
77
127
|
: conversationId}
|
|
78
128
|
</span>
|
|
79
129
|
|
|
130
|
+
{/* User-Agent */}
|
|
131
|
+
{userAgent !== null && userAgent !== undefined && userAgent !== "" && (
|
|
132
|
+
<span
|
|
133
|
+
className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
|
|
134
|
+
title={userAgent}
|
|
135
|
+
>
|
|
136
|
+
<User className="size-3" />
|
|
137
|
+
<span className="font-mono tabular-nums truncate max-w-[120px]">{userAgent}</span>
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
|
|
80
141
|
{/* API Format Badge */}
|
|
81
142
|
{!hideApiFormat && (
|
|
82
143
|
<Badge
|
|
@@ -2,9 +2,16 @@ import { Check, Copy, GitCompareArrows, RotateCcw } from "lucide-react";
|
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import { useMemo, useState, memo } from "react";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type CapturedLog,
|
|
7
|
+
parseRequest,
|
|
8
|
+
InspectorResponseSchema,
|
|
9
|
+
parseOpenAIResponse,
|
|
10
|
+
OpenAIRequestSchema,
|
|
11
|
+
} from "../../proxy/schemas";
|
|
6
12
|
import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
|
|
7
13
|
import { Button } from "../ui/button";
|
|
14
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
8
15
|
import { JsonViewerFromString } from "../ui/json-viewer";
|
|
9
16
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
10
17
|
import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
|
|
@@ -17,10 +24,7 @@ import type { CacheTrendEntry } from "./cacheTrend";
|
|
|
17
24
|
export type LogEntryProps = {
|
|
18
25
|
log: CapturedLog;
|
|
19
26
|
viewMode?: "simple" | "full";
|
|
20
|
-
/**
|
|
21
|
-
suppressApiFormatBadge?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Live "strip Claude Code billing header" flag, sourced once at the viewer
|
|
27
|
+
/** Live "strip Claude Code billing header" flag, sourced once at the viewer
|
|
24
28
|
* container. Hoisted out of `LogEntry` so a single SWR subscription serves
|
|
25
29
|
* the whole virtualized list (N logs == N subscriptions is the previous
|
|
26
30
|
* cost).
|
|
@@ -31,10 +35,8 @@ export type LogEntryProps = {
|
|
|
31
35
|
* `null` (or absent) means the header should render with no arrows.
|
|
32
36
|
*/
|
|
33
37
|
cacheTrend?: CacheTrendEntry | null;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
/** Toggle this log in/out of the comparison selection. */
|
|
37
|
-
onToggleSelect?: (logId: number) => void;
|
|
38
|
+
/** Callback to open CompareDrawer with this log and its immediate predecessor. */
|
|
39
|
+
onCompareWithPrevious?: () => void;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -118,32 +120,38 @@ function DiffToggleButton({
|
|
|
118
120
|
onClick: (e: React.MouseEvent) => void;
|
|
119
121
|
}): JSX.Element {
|
|
120
122
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
<TooltipProvider>
|
|
124
|
+
<Tooltip>
|
|
125
|
+
<TooltipTrigger asChild>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={onClick}
|
|
129
|
+
aria-pressed={active}
|
|
130
|
+
className={cn(
|
|
131
|
+
"flex items-center gap-1.5 text-xs px-2 py-1 rounded transition-colors",
|
|
132
|
+
active
|
|
133
|
+
? "bg-primary/10 text-primary"
|
|
134
|
+
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
<GitCompareArrows className="size-3" />
|
|
138
|
+
{active ? "Showing diff" : "Diff with Raw"}
|
|
139
|
+
</button>
|
|
140
|
+
</TooltipTrigger>
|
|
141
|
+
<TooltipContent>
|
|
142
|
+
{active ? "Hide diff view" : "Compare proxy output against the original raw version"}
|
|
143
|
+
</TooltipContent>
|
|
144
|
+
</Tooltip>
|
|
145
|
+
</TooltipProvider>
|
|
136
146
|
);
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
export const LogEntry = memo(function ({
|
|
140
150
|
log,
|
|
141
151
|
viewMode = "simple",
|
|
142
|
-
suppressApiFormatBadge = false,
|
|
143
152
|
strip,
|
|
144
153
|
cacheTrend = null,
|
|
145
|
-
|
|
146
|
-
onToggleSelect,
|
|
154
|
+
onCompareWithPrevious,
|
|
147
155
|
}: LogEntryProps): JSX.Element {
|
|
148
156
|
const [expanded, setExpanded] = useState<boolean>(false);
|
|
149
157
|
const [requestCopied, setRequestCopied] = useState<boolean>(false);
|
|
@@ -152,7 +160,63 @@ export const LogEntry = memo(function ({
|
|
|
152
160
|
const [replayOpen, setReplayOpen] = useState<boolean>(false);
|
|
153
161
|
const [headersDiff, setHeadersDiff] = useState<boolean>(false);
|
|
154
162
|
const [requestDiff, setRequestDiff] = useState<boolean>(false);
|
|
155
|
-
const
|
|
163
|
+
const messageCount = useMemo(() => {
|
|
164
|
+
if (log.rawRequestBody === null) return null;
|
|
165
|
+
if (log.apiFormat === "anthropic") {
|
|
166
|
+
const parsed = parseRequest(log.rawRequestBody);
|
|
167
|
+
if (parsed !== null) return parsed.messages.length;
|
|
168
|
+
} else if (log.apiFormat === "openai") {
|
|
169
|
+
try {
|
|
170
|
+
const result = OpenAIRequestSchema.safeParse(JSON.parse(log.rawRequestBody));
|
|
171
|
+
if (result.success) return result.data.messages.length;
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}, [log.rawRequestBody, log.apiFormat]);
|
|
178
|
+
const toolCount = useMemo(() => {
|
|
179
|
+
if (log.rawRequestBody === null) return null;
|
|
180
|
+
if (log.apiFormat === "anthropic") {
|
|
181
|
+
const parsed = parseRequest(log.rawRequestBody);
|
|
182
|
+
if (parsed !== null && parsed.tools !== undefined && parsed.tools.length > 0) {
|
|
183
|
+
return parsed.tools.length;
|
|
184
|
+
}
|
|
185
|
+
} else if (log.apiFormat === "openai") {
|
|
186
|
+
try {
|
|
187
|
+
const result = OpenAIRequestSchema.safeParse(JSON.parse(log.rawRequestBody));
|
|
188
|
+
if (result.success && result.data.tools !== undefined && result.data.tools.length > 0) {
|
|
189
|
+
return result.data.tools.length;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// ignore
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}, [log.rawRequestBody, log.apiFormat]);
|
|
197
|
+
const responseToolNames = useMemo(() => {
|
|
198
|
+
if (log.responseText === null) return null;
|
|
199
|
+
if (log.apiFormat === "openai") {
|
|
200
|
+
const parsed = parseOpenAIResponse(log.responseText);
|
|
201
|
+
if (parsed !== null) {
|
|
202
|
+
const toolCalls = parsed.choices[0]?.message?.tool_calls;
|
|
203
|
+
if (toolCalls !== undefined && toolCalls !== null && toolCalls.length > 0) {
|
|
204
|
+
return toolCalls.map((tc) => tc.function?.name ?? "?").filter((n) => n !== "");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (log.apiFormat === "anthropic") {
|
|
208
|
+
try {
|
|
209
|
+
const result = InspectorResponseSchema.safeParse(JSON.parse(log.responseText));
|
|
210
|
+
if (result.success) {
|
|
211
|
+
const names = result.data.content.filter((c) => c.type === "tool_use").map((c) => c.name);
|
|
212
|
+
if (names.length > 0) return names;
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// JSON parse failed, ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}, [log.responseText, log.apiFormat]);
|
|
156
220
|
const strippedRequestBody = useMemo(() => {
|
|
157
221
|
if (!strip || log.apiFormat !== "anthropic" || log.rawRequestBody === null) {
|
|
158
222
|
return null;
|
|
@@ -199,34 +263,70 @@ export const LogEntry = memo(function ({
|
|
|
199
263
|
|
|
200
264
|
return (
|
|
201
265
|
<>
|
|
202
|
-
<div
|
|
203
|
-
className={cn(
|
|
204
|
-
"border border-border rounded-lg mb-3 overflow-hidden",
|
|
205
|
-
isSelected && "border-l-2 border-l-amber-400",
|
|
206
|
-
)}
|
|
207
|
-
>
|
|
266
|
+
<div className="border border-border rounded-lg mb-3 overflow-hidden">
|
|
208
267
|
<LogEntryHeader
|
|
209
268
|
log={log}
|
|
210
|
-
|
|
269
|
+
messageCount={messageCount}
|
|
270
|
+
toolCount={toolCount}
|
|
211
271
|
expanded={expanded}
|
|
212
272
|
onToggle={() => setExpanded(!expanded)}
|
|
213
|
-
|
|
273
|
+
responseToolNames={responseToolNames}
|
|
214
274
|
cacheTrend={cacheTrend}
|
|
215
|
-
isSelected={isSelected}
|
|
216
|
-
onToggleSelect={onToggleSelect}
|
|
217
275
|
/>
|
|
218
276
|
|
|
219
277
|
{expanded && (
|
|
220
278
|
<div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
|
|
221
279
|
<Tabs defaultValue="request">
|
|
222
280
|
<TabsList className="mx-4 mt-2">
|
|
223
|
-
{viewMode === "full" &&
|
|
224
|
-
|
|
281
|
+
{viewMode === "full" && (
|
|
282
|
+
<TooltipProvider>
|
|
283
|
+
<Tooltip>
|
|
284
|
+
<TooltipTrigger asChild>
|
|
285
|
+
<TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>
|
|
286
|
+
</TooltipTrigger>
|
|
287
|
+
<TooltipContent>
|
|
288
|
+
HTTP headers received from the upstream provider
|
|
289
|
+
</TooltipContent>
|
|
290
|
+
</Tooltip>
|
|
291
|
+
</TooltipProvider>
|
|
292
|
+
)}
|
|
293
|
+
{viewMode === "full" && (
|
|
294
|
+
<TooltipProvider>
|
|
295
|
+
<Tooltip>
|
|
296
|
+
<TooltipTrigger asChild>
|
|
297
|
+
<TabsTrigger value="headers">Headers</TabsTrigger>
|
|
298
|
+
</TooltipTrigger>
|
|
299
|
+
<TooltipContent>
|
|
300
|
+
Request and response headers sent and received
|
|
301
|
+
</TooltipContent>
|
|
302
|
+
</Tooltip>
|
|
303
|
+
</TooltipProvider>
|
|
304
|
+
)}
|
|
225
305
|
{shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && (
|
|
226
|
-
<
|
|
306
|
+
<TooltipProvider>
|
|
307
|
+
<Tooltip>
|
|
308
|
+
<TooltipTrigger asChild>
|
|
309
|
+
<TabsTrigger value="raw-request">Raw Request</TabsTrigger>
|
|
310
|
+
</TooltipTrigger>
|
|
311
|
+
<TooltipContent>
|
|
312
|
+
Exact HTTP request sent to the upstream provider
|
|
313
|
+
</TooltipContent>
|
|
314
|
+
</Tooltip>
|
|
315
|
+
</TooltipProvider>
|
|
227
316
|
)}
|
|
228
317
|
<TabsTrigger value="request">Request</TabsTrigger>
|
|
229
|
-
{viewMode === "full" &&
|
|
318
|
+
{viewMode === "full" && (
|
|
319
|
+
<TooltipProvider>
|
|
320
|
+
<Tooltip>
|
|
321
|
+
<TooltipTrigger asChild>
|
|
322
|
+
<TabsTrigger value="raw">Raw Response</TabsTrigger>
|
|
323
|
+
</TooltipTrigger>
|
|
324
|
+
<TooltipContent>
|
|
325
|
+
Exact HTTP response from the upstream provider
|
|
326
|
+
</TooltipContent>
|
|
327
|
+
</Tooltip>
|
|
328
|
+
</TooltipProvider>
|
|
329
|
+
)}
|
|
230
330
|
<TabsTrigger value="parsed">Response</TabsTrigger>
|
|
231
331
|
</TabsList>
|
|
232
332
|
|
|
@@ -267,18 +367,48 @@ export const LogEntry = memo(function ({
|
|
|
267
367
|
}}
|
|
268
368
|
/>
|
|
269
369
|
)}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
370
|
+
{onCompareWithPrevious !== undefined && (
|
|
371
|
+
<TooltipProvider>
|
|
372
|
+
<Tooltip>
|
|
373
|
+
<TooltipTrigger asChild>
|
|
374
|
+
<Button
|
|
375
|
+
variant="outline"
|
|
376
|
+
size="sm"
|
|
377
|
+
className="h-7 text-xs"
|
|
378
|
+
onClick={(e) => {
|
|
379
|
+
e.stopPropagation();
|
|
380
|
+
onCompareWithPrevious();
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<GitCompareArrows className="size-3 mr-1" />
|
|
384
|
+
Diff with Previous
|
|
385
|
+
</Button>
|
|
386
|
+
</TooltipTrigger>
|
|
387
|
+
<TooltipContent>
|
|
388
|
+
Compare this request with the immediately preceding one
|
|
389
|
+
</TooltipContent>
|
|
390
|
+
</Tooltip>
|
|
391
|
+
</TooltipProvider>
|
|
392
|
+
)}
|
|
393
|
+
<TooltipProvider>
|
|
394
|
+
<Tooltip>
|
|
395
|
+
<TooltipTrigger asChild>
|
|
396
|
+
<Button
|
|
397
|
+
variant="outline"
|
|
398
|
+
size="sm"
|
|
399
|
+
className="h-7 text-xs"
|
|
400
|
+
onClick={(e) => {
|
|
401
|
+
e.stopPropagation();
|
|
402
|
+
setReplayOpen(true);
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<RotateCcw className="size-3 mr-1" />
|
|
406
|
+
Replay
|
|
407
|
+
</Button>
|
|
408
|
+
</TooltipTrigger>
|
|
409
|
+
<TooltipContent>Re-send this request to the provider</TooltipContent>
|
|
410
|
+
</Tooltip>
|
|
411
|
+
</TooltipProvider>
|
|
282
412
|
<CopyButton
|
|
283
413
|
text={displayedRequestBody}
|
|
284
414
|
label="Copy Request"
|