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 { fn } from "storybook/test";
import { ContentViewer } from "./ContentViewer";
const meta: Meta<typeof ContentViewer> = {
@ -74,6 +73,15 @@ export const WithCopyButton: Story = {
args: {
type: "text",
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 {
type: "text" | "json" | "image" | "audio";
content: string;
mimeType?: string;
onCopy?: () => void;
copyable?: boolean;
}
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({
type,
content,
mimeType,
onCopy,
copyable = false,
}: ContentViewerProps) {
const showCopy = copyable && (type === "text" || type === "json");
return (
<Stack gap="xs">
{type === "json" && <Code block>{formatJson(content)}</Code>}
{type === "text" && <Code block>{content}</Code>}
{(type === "json" || type === "text") && (
<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" && (
<Image
src={`data:${mimeType || "image/png"};base64,${content}`}
@ -38,11 +56,6 @@ export function ContentViewer({
<source src={`data:${mimeType || "audio/wav"};base64,${content}`} />
</audio>
)}
{onCopy && (
<Button variant="light" size="xs" onClick={onCopy}>
Copy
</Button>
)}
</Stack>
);
}

View file

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

View file

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