refactor atom components to use .withProps() subcomponents and extract inline code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fb4ca78726
commit
04c23094a4
7 changed files with 168 additions and 82 deletions
|
|
@ -16,11 +16,31 @@ function formatJson(content: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDataUri(mimeType: string, content: string): string {
|
||||||
|
return `data:${mimeType};base64,${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatContent(type: "text" | "json", content: string): string {
|
||||||
|
return type === "json" ? formatJson(content) : content;
|
||||||
|
}
|
||||||
|
|
||||||
const ContentWrapper = Flex.withProps({
|
const ContentWrapper = Flex.withProps({
|
||||||
pos: "relative",
|
pos: "relative",
|
||||||
direction: "column",
|
direction: "column",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CopyOverlay = Flex.withProps({
|
||||||
|
pos: "absolute",
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
const PreviewImage = Image.withProps({
|
||||||
|
alt: "Content preview",
|
||||||
|
maw: 400,
|
||||||
|
radius: "md",
|
||||||
|
});
|
||||||
|
|
||||||
export function ContentViewer({
|
export function ContentViewer({
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
|
@ -34,26 +54,21 @@ export function ContentViewer({
|
||||||
{(type === "json" || type === "text") && (
|
{(type === "json" || type === "text") && (
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<Code block p={36}>
|
<Code block p={36}>
|
||||||
{type === "json" ? formatJson(content) : content}
|
{formatContent(type, content)}
|
||||||
</Code>
|
</Code>
|
||||||
{showCopy && (
|
{showCopy && (
|
||||||
<Flex pos="absolute" top={4} right={4}>
|
<CopyOverlay>
|
||||||
<CopyButton value={content} />
|
<CopyButton value={content} />
|
||||||
</Flex>
|
</CopyOverlay>
|
||||||
)}
|
)}
|
||||||
</ContentWrapper>
|
</ContentWrapper>
|
||||||
)}
|
)}
|
||||||
{type === "image" && (
|
{type === "image" && (
|
||||||
<Image
|
<PreviewImage src={buildDataUri(mimeType || "image/png", content)} />
|
||||||
src={`data:${mimeType || "image/png"};base64,${content}`}
|
|
||||||
alt="Content preview"
|
|
||||||
maw={400}
|
|
||||||
radius="md"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{type === "audio" && (
|
{type === "audio" && (
|
||||||
<audio controls>
|
<audio controls>
|
||||||
<source src={`data:${mimeType || "audio/wav"};base64,${content}`} />
|
<source src={buildDataUri(mimeType || "audio/wav", content)} />
|
||||||
</audio>
|
</audio>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,46 @@ export interface InlineErrorProps {
|
||||||
docLink?: string;
|
docLink?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HeaderRow = Group.withProps({
|
||||||
|
justify: "space-between",
|
||||||
|
wrap: "nowrap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const MessageGroup = Group.withProps({
|
||||||
|
gap: "sm",
|
||||||
|
wrap: "nowrap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ErrorMessage = Text.withProps({
|
||||||
|
size: "sm",
|
||||||
|
fw: 500,
|
||||||
|
c: "red",
|
||||||
|
});
|
||||||
|
|
||||||
|
const RetryText = Text.withProps({
|
||||||
|
size: "sm",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ExpandButton = Button.withProps({
|
||||||
|
variant: "transparent",
|
||||||
|
size: "compact-xs",
|
||||||
|
c: "var(--inspector-text-primary)",
|
||||||
|
});
|
||||||
|
|
||||||
|
const DocLinkButton = Button.withProps({
|
||||||
|
variant: "light",
|
||||||
|
size: "xs",
|
||||||
|
component: "a",
|
||||||
|
target: "_blank",
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatRetryLabel(retryCount: number, maxRetries?: number): string {
|
||||||
|
return maxRetries !== undefined
|
||||||
|
? `Retry attempt ${retryCount} of ${maxRetries}`
|
||||||
|
: `Retry attempt ${retryCount}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function InlineError({
|
export function InlineError({
|
||||||
message,
|
message,
|
||||||
details,
|
details,
|
||||||
|
|
@ -22,42 +62,26 @@ export function InlineError({
|
||||||
return (
|
return (
|
||||||
<Alert color="red" variant="light">
|
<Alert color="red" variant="light">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<HeaderRow>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<MessageGroup>
|
||||||
<Text size="sm" fw={500} c="red">
|
<ErrorMessage>{message}</ErrorMessage>
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
{retryCount !== undefined && (
|
{retryCount !== undefined && (
|
||||||
<Text size="sm" c="dimmed">
|
<RetryText>{formatRetryLabel(retryCount, maxRetries)}</RetryText>
|
||||||
Retry attempt {retryCount}
|
|
||||||
{maxRetries !== undefined && ` of ${maxRetries}`}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Group>
|
</MessageGroup>
|
||||||
{hasExpandable && (
|
{hasExpandable && (
|
||||||
<Button
|
<ExpandButton onClick={() => setExpanded((v) => !v)}>
|
||||||
variant="transparent"
|
|
||||||
size="compact-xs"
|
|
||||||
c="var(--inspector-text-primary)"
|
|
||||||
onClick={() => setExpanded((v) => !v)}
|
|
||||||
>
|
|
||||||
{expanded ? "Show less" : "Show more"}
|
{expanded ? "Show less" : "Show more"}
|
||||||
</Button>
|
</ExpandButton>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</HeaderRow>
|
||||||
<Collapse in={expanded}>
|
<Collapse in={expanded}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{details && <Text size="sm">{details}</Text>}
|
{details && <Text size="sm">{details}</Text>}
|
||||||
{docLink && (
|
{docLink && (
|
||||||
<Button
|
<DocLinkButton href={docLink}>
|
||||||
variant="light"
|
|
||||||
size="xs"
|
|
||||||
component="a"
|
|
||||||
href={docLink}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
View Troubleshooting Guide
|
View Troubleshooting Guide
|
||||||
</Button>
|
</DocLinkButton>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
import { Button, Flex, Group, Text } from "@mantine/core";
|
import { Button, Group, Paper, Text } from "@mantine/core";
|
||||||
|
|
||||||
export interface ListChangedIndicatorProps {
|
export interface ListChangedIndicatorProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dot = Flex.withProps({
|
const Dot = Paper.withProps({
|
||||||
w: 8,
|
w: 8,
|
||||||
h: 8,
|
h: 8,
|
||||||
style: { borderRadius: "50%" },
|
radius: "xl",
|
||||||
bg: "var(--inspector-status-connecting)",
|
bg: "var(--inspector-status-connecting)",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const UpdateLabel = Text.withProps({
|
||||||
|
size: "sm",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const RefreshButton = Button.withProps({
|
||||||
|
size: "xs",
|
||||||
|
variant: "light",
|
||||||
|
});
|
||||||
|
|
||||||
export function ListChangedIndicator({
|
export function ListChangedIndicator({
|
||||||
visible,
|
visible,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
|
@ -21,12 +31,8 @@ export function ListChangedIndicator({
|
||||||
return (
|
return (
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Dot />
|
<Dot />
|
||||||
<Text size="sm" c="dimmed">
|
<UpdateLabel>List updated</UpdateLabel>
|
||||||
List updated
|
<RefreshButton onClick={onRefresh}>↻ Refresh</RefreshButton>
|
||||||
</Text>
|
|
||||||
<Button size="xs" variant="light" onClick={onRefresh}>
|
|
||||||
↻ Refresh
|
|
||||||
</Button>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,24 +54,37 @@ function isBoldLevel(level: LogLevel): boolean {
|
||||||
return level === "alert" || level === "emergency";
|
return level === "alert" || level === "emergency";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBoldWeight(level: LogLevel): number | undefined {
|
||||||
|
return isBoldLevel(level) ? 700 : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLogger(logger: string): string {
|
||||||
|
return `[${logger}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimestampText = Text.withProps({
|
||||||
|
size: "sm",
|
||||||
|
ff: "monospace",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoggerText = Text.withProps({
|
||||||
|
size: "xs",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
export function LogEntry({ timestamp, level, message, logger }: LogEntryProps) {
|
export function LogEntry({ timestamp, level, message, logger }: LogEntryProps) {
|
||||||
return (
|
return (
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Text size="sm" ff="monospace" c="dimmed">
|
<TimestampText>{timestamp}</TimestampText>
|
||||||
{timestamp}
|
|
||||||
</Text>
|
|
||||||
<Badge
|
<Badge
|
||||||
color={levelBadgeColor[level]}
|
color={levelBadgeColor[level]}
|
||||||
variant={levelBadgeVariant[level]}
|
variant={levelBadgeVariant[level]}
|
||||||
fw={isBoldLevel(level) ? 700 : undefined}
|
fw={getBoldWeight(level)}
|
||||||
>
|
>
|
||||||
{level}
|
{level}
|
||||||
</Badge>
|
</Badge>
|
||||||
{logger && (
|
{logger && <LoggerText>{formatLogger(logger)}</LoggerText>}
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
[{logger}]
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Text size="sm" c={levelMessageColor[level]}>
|
<Text size="sm" c={levelMessageColor[level]}>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,35 @@ export interface MessageBubbleProps {
|
||||||
audioContent?: { data: string; mimeType: string };
|
audioContent?: { data: string; mimeType: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDataUri(mimeType: string, data: string): string {
|
||||||
|
return `data:${mimeType};base64,${data}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRoleLabel(index: number, role: string): string {
|
||||||
|
return `[${index}] role: ${role}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatQuotedContent(content: string): string {
|
||||||
|
return `"${content}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BubbleContainer = Paper.withProps({
|
||||||
|
p: "md",
|
||||||
|
radius: "md",
|
||||||
|
withBorder: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const RoleLabel = Text.withProps({
|
||||||
|
size: "xs",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const PreviewImage = Image.withProps({
|
||||||
|
maw: 300,
|
||||||
|
radius: "sm",
|
||||||
|
mt: "xs",
|
||||||
|
});
|
||||||
|
|
||||||
export function MessageBubble({
|
export function MessageBubble({
|
||||||
index,
|
index,
|
||||||
role,
|
role,
|
||||||
|
|
@ -17,32 +46,27 @@ export function MessageBubble({
|
||||||
audioContent,
|
audioContent,
|
||||||
}: MessageBubbleProps) {
|
}: MessageBubbleProps) {
|
||||||
return (
|
return (
|
||||||
<Paper p="md" radius="md" withBorder>
|
<BubbleContainer>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Text size="xs" c="dimmed">
|
<RoleLabel>{formatRoleLabel(index, role)}</RoleLabel>
|
||||||
[{index}] role: {role}
|
|
||||||
</Text>
|
|
||||||
<CopyButton value={content} />
|
<CopyButton value={content} />
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="sm">"{content}"</Text>
|
<Text size="sm">{formatQuotedContent(content)}</Text>
|
||||||
{imageContent && (
|
{imageContent && (
|
||||||
<Image
|
<PreviewImage
|
||||||
src={`data:${imageContent.mimeType};base64,${imageContent.data}`}
|
src={buildDataUri(imageContent.mimeType, imageContent.data)}
|
||||||
maw={300}
|
|
||||||
radius="sm"
|
|
||||||
mt="xs"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{audioContent && (
|
{audioContent && (
|
||||||
<audio controls>
|
<audio controls>
|
||||||
<source
|
<source
|
||||||
src={`data:${audioContent.mimeType};base64,${audioContent.data}`}
|
src={buildDataUri(audioContent.mimeType, audioContent.data)}
|
||||||
type={audioContent.mimeType}
|
type={audioContent.mimeType}
|
||||||
/>
|
/>
|
||||||
</audio>
|
</audio>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</BubbleContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,20 @@ export interface ProgressDisplayProps {
|
||||||
elapsed?: string;
|
elapsed?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPercent(progress: number): string {
|
||||||
|
return `${progress}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgressLabel = Text.withProps({
|
||||||
|
size: "sm",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ElapsedText = Text.withProps({
|
||||||
|
size: "xs",
|
||||||
|
c: "dimmed",
|
||||||
|
});
|
||||||
|
|
||||||
export function ProgressDisplay({
|
export function ProgressDisplay({
|
||||||
progress,
|
progress,
|
||||||
description,
|
description,
|
||||||
|
|
@ -14,21 +28,11 @@ export function ProgressDisplay({
|
||||||
return (
|
return (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
{description && (
|
{description && <ProgressLabel>{description}</ProgressLabel>}
|
||||||
<Text size="sm" c="dimmed">
|
<ProgressLabel>{formatPercent(progress)}</ProgressLabel>
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{progress}%
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Progress value={progress} size="sm" />
|
<Progress value={progress} size="sm" />
|
||||||
{elapsed && (
|
{elapsed && <ElapsedText>{elapsed}</ElapsedText>}
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{elapsed}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Flex, Group, Text } from "@mantine/core";
|
import { Group, Paper, Text } from "@mantine/core";
|
||||||
|
|
||||||
export interface StatusIndicatorProps {
|
export interface StatusIndicatorProps {
|
||||||
status: "connected" | "connecting" | "disconnected" | "failed";
|
status: "connected" | "connecting" | "disconnected" | "failed";
|
||||||
|
|
@ -30,10 +30,10 @@ function getLabel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dot = Flex.withProps({
|
const Dot = Paper.withProps({
|
||||||
w: 10,
|
w: 10,
|
||||||
h: 10,
|
h: 10,
|
||||||
style: { borderRadius: "50%" },
|
radius: "xl",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function StatusIndicator({
|
export function StatusIndicator({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue