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