Refactor renamed the component groups to something less obtuse:

Elements -> Groups -> Screens -> Views
This commit is contained in:
cliffhall 2026-03-28 12:58:47 -04:00
commit dd46acba87
2 changed files with 151 additions and 144 deletions

View file

@ -0,0 +1,108 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { HistoryEntry } from "./HistoryEntry";
const meta: Meta<typeof HistoryEntry> = {
title: "Groups/HistoryEntry",
component: HistoryEntry,
args: {
onToggleExpand: fn(),
onReplay: fn(),
onTogglePin: fn(),
},
};
export default meta;
type Story = StoryObj<typeof HistoryEntry>;
export const SuccessCollapsed: Story = {
args: {
timestamp: "2026-03-17T10:30:00Z",
method: "tools/call",
target: "get_weather",
status: "success",
durationMs: 142,
isPinned: false,
isExpanded: false,
},
};
export const SuccessExpanded: Story = {
args: {
timestamp: "2026-03-17T10:30:00Z",
method: "tools/call",
target: "get_weather",
status: "success",
durationMs: 142,
isPinned: false,
isExpanded: true,
parameters: {
city: "San Francisco",
units: "celsius",
},
response: {
temperature: 18,
conditions: "Partly cloudy",
humidity: 65,
},
},
};
export const Error: Story = {
args: {
timestamp: "2026-03-17T10:31:15Z",
method: "tools/call",
target: "query_database",
status: "error",
durationMs: 3200,
isPinned: false,
isExpanded: false,
},
};
export const WithChildren: Story = {
args: {
timestamp: "2026-03-17T10:32:00Z",
method: "tools/call",
target: "complex_operation",
status: "success",
durationMs: 1250,
isPinned: false,
isExpanded: true,
parameters: {
action: "process",
},
response: {
result: "completed",
},
childEntries: [
{
timestamp: "+120ms",
method: "sampling/createMessage",
target: "gpt-4",
status: "success",
durationMs: 800,
},
{
timestamp: "+950ms",
method: "elicitation/create",
target: "confirm_action",
status: "success",
durationMs: 200,
},
],
},
};
export const Pinned: Story = {
args: {
timestamp: "2026-03-17T10:33:00Z",
method: "resources/read",
target: "config.json",
status: "success",
durationMs: 45,
isPinned: true,
pinLabel: "Baseline",
isExpanded: false,
},
};

View file

@ -0,0 +1,182 @@
import {
Badge,
Button,
Card,
Collapse,
Group,
Stack,
Text,
} from "@mantine/core";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
export interface HistoryChildEntry {
timestamp: string;
method: string;
target?: string;
status: "success" | "error";
durationMs: number;
}
export interface HistoryEntryProps {
timestamp: string;
method: string;
target?: string;
status: "success" | "error";
durationMs: number;
parameters?: Record<string, unknown>;
response?: Record<string, unknown>;
childEntries?: HistoryChildEntry[];
isPinned: boolean;
pinLabel?: string;
isExpanded: boolean;
onToggleExpand: () => void;
onReplay: () => void;
onTogglePin: () => void;
}
const EntryContainer = Card.withProps({
withBorder: true,
padding: "md",
});
const HeaderRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const TimestampText = Text.withProps({
size: "sm",
c: "dimmed",
ff: "monospace",
});
const TargetText = Text.withProps({
size: "sm",
fw: 500,
});
const DurationText = Text.withProps({
size: "sm",
c: "dimmed",
});
const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
});
const ChildMethodBadge = Badge.withProps({
color: "dark",
size: "sm",
});
function formatDuration(ms: number): string {
return `${ms}ms`;
}
function formatStatusLabel(status: "success" | "error"): string {
return status === "success" ? "OK" : "Error";
}
function statusColor(status: "success" | "error"): string {
return status === "success" ? "green" : "red";
}
function formatPinLabel(isPinned: boolean, pinLabel?: string): string {
const base = isPinned ? "Unpin" : "Pin";
return isPinned && pinLabel ? `${base} (${pinLabel})` : base;
}
function serializeJson(value: Record<string, unknown>): string {
return JSON.stringify(value);
}
export function HistoryEntry({
timestamp,
method,
target,
status,
durationMs,
parameters,
response,
childEntries,
isPinned,
pinLabel,
isExpanded,
onToggleExpand,
onReplay,
onTogglePin,
}: HistoryEntryProps) {
return (
<EntryContainer>
<Stack gap="sm">
<HeaderRow>
<Group gap="sm">
<TimestampText>{timestamp}</TimestampText>
<Badge color="dark">{method}</Badge>
{target && <TargetText>{target}</TargetText>}
</Group>
<Group gap="sm">
<DurationText>{formatDuration(durationMs)}</DurationText>
<Badge color={statusColor(status)}>
{formatStatusLabel(status)}
</Badge>
</Group>
</HeaderRow>
<Group gap="xs">
<SubtleButton onClick={onReplay}>Replay</SubtleButton>
<SubtleButton onClick={onTogglePin}>
{formatPinLabel(isPinned, pinLabel)}
</SubtleButton>
<SubtleButton onClick={onToggleExpand} ml="auto">
{isExpanded ? "Collapse" : "Expand"}
</SubtleButton>
</Group>
<Collapse in={isExpanded}>
<Stack gap="sm">
{parameters && (
<Stack gap="xs">
<Text size="sm">Parameters:</Text>
<ContentViewer
type="json"
content={serializeJson(parameters)}
copyable
/>
</Stack>
)}
{response && (
<Stack gap="xs">
<Text size="sm">Response:</Text>
<ContentViewer
type="json"
content={serializeJson(response)}
copyable
/>
</Stack>
)}
{childEntries && childEntries.length > 0 && (
<Stack gap="xs">
{childEntries.map((child, index) => (
<Group key={index} pl="lg" gap="sm">
<TimestampText>+-- </TimestampText>
<TimestampText>{child.timestamp}</TimestampText>
<ChildMethodBadge>{child.method}</ChildMethodBadge>
{child.target && <Text size="sm">{child.target}</Text>}
<Badge color={statusColor(child.status)} size="sm">
{formatStatusLabel(child.status)}
</Badge>
<DurationText>
{formatDuration(child.durationMs)}
</DurationText>
</Group>
))}
</Stack>
)}
</Stack>
</Collapse>
</Stack>
</EntryContainer>
);
}