fix HistoryEntry children prop conflict and overlay CopyButton in ContentViewer

Rename HistoryEntry's children prop to childEntries to avoid React's
reserved prop name. Replace ContentViewer's onCopy callback with a
copyable flag and overlay CopyButton in the top-right corner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-22 16:03:56 -04:00
commit 8d434e2381
4 changed files with 38 additions and 17 deletions

View file

@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ContentViewer } from "./ContentViewer"; import { ContentViewer } from "./ContentViewer";
const meta: Meta<typeof ContentViewer> = { const meta: Meta<typeof ContentViewer> = {
@ -74,6 +73,15 @@ export const WithCopyButton: Story = {
args: { args: {
type: "text", type: "text",
content: "Hello, world!\nThis text can be copied.", content: "Hello, world!\nThis text can be copied.",
onCopy: fn(), copyable: true,
},
};
export const JsonWithCopy: Story = {
args: {
type: "json",
content:
'{"name":"my-app","version":"1.0.0","description":"Sample configuration file"}',
copyable: true,
}, },
}; };

View file

@ -1,10 +1,11 @@
import { Button, Code, Image, Stack } from "@mantine/core"; import { Code, Flex, Image, Stack } from "@mantine/core";
import { CopyButton } from "../CopyButton/CopyButton";
export interface ContentViewerProps { export interface ContentViewerProps {
type: "text" | "json" | "image" | "audio"; type: "text" | "json" | "image" | "audio";
content: string; content: string;
mimeType?: string; mimeType?: string;
onCopy?: () => void; copyable?: boolean;
} }
function formatJson(content: string): string { function formatJson(content: string): string {
@ -15,16 +16,33 @@ function formatJson(content: string): string {
} }
} }
const ContentWrapper = Flex.withProps({
pos: "relative",
direction: "column",
});
export function ContentViewer({ export function ContentViewer({
type, type,
content, content,
mimeType, mimeType,
onCopy, copyable = false,
}: ContentViewerProps) { }: ContentViewerProps) {
const showCopy = copyable && (type === "text" || type === "json");
return ( return (
<Stack gap="xs"> <Stack gap="xs">
{type === "json" && <Code block>{formatJson(content)}</Code>} {(type === "json" || type === "text") && (
{type === "text" && <Code block>{content}</Code>} <ContentWrapper>
<Code block p={36}>
{type === "json" ? formatJson(content) : content}
</Code>
{showCopy && (
<Flex pos="absolute" top={4} right={4}>
<CopyButton value={content} />
</Flex>
)}
</ContentWrapper>
)}
{type === "image" && ( {type === "image" && (
<Image <Image
src={`data:${mimeType || "image/png"};base64,${content}`} src={`data:${mimeType || "image/png"};base64,${content}`}
@ -38,11 +56,6 @@ export function ContentViewer({
<source src={`data:${mimeType || "audio/wav"};base64,${content}`} /> <source src={`data:${mimeType || "audio/wav"};base64,${content}`} />
</audio> </audio>
)} )}
{onCopy && (
<Button variant="light" size="xs" onClick={onCopy}>
Copy
</Button>
)}
</Stack> </Stack>
); );
} }

View file

@ -75,7 +75,7 @@ export const WithChildren: Story = {
response: { response: {
result: "completed", result: "completed",
}, },
children: [ childEntries: [
{ {
timestamp: "+120ms", timestamp: "+120ms",
method: "sampling/createMessage", method: "sampling/createMessage",

View file

@ -25,7 +25,7 @@ export interface HistoryEntryProps {
durationMs: number; durationMs: number;
parameters?: Record<string, unknown>; parameters?: Record<string, unknown>;
response?: Record<string, unknown>; response?: Record<string, unknown>;
children?: HistoryChildEntry[]; childEntries?: HistoryChildEntry[];
isPinned: boolean; isPinned: boolean;
pinLabel?: string; pinLabel?: string;
isExpanded: boolean; isExpanded: boolean;
@ -42,7 +42,7 @@ export function HistoryEntry({
durationMs, durationMs,
parameters, parameters,
response, response,
children, childEntries,
isPinned, isPinned,
pinLabel, pinLabel,
isExpanded, isExpanded,
@ -102,9 +102,9 @@ export function HistoryEntry({
<Code block>{JSON.stringify(response, null, 2)}</Code> <Code block>{JSON.stringify(response, null, 2)}</Code>
</Stack> </Stack>
)} )}
{children && children.length > 0 && ( {childEntries && childEntries.length > 0 && (
<Stack gap="xs"> <Stack gap="xs">
{children.map((child, index) => ( {childEntries.map((child, index) => (
<Group key={index} pl="lg" gap="sm"> <Group key={index} pl="lg" gap="sm">
<Text size="sm" c="dimmed" ff="monospace"> <Text size="sm" c="dimmed" ff="monospace">
+--{" "} +--{" "}