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:
parent
5d35a5f94c
commit
8d434e2381
4 changed files with 38 additions and 17 deletions
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
+--{" "}
|
+--{" "}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue