diff --git a/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx b/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx
index ef56384e..d9b6bb02 100644
--- a/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx
+++ b/clients/web/src/components/atoms/ContentViewer/ContentViewer.tsx
@@ -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") && (
- {type === "json" ? formatJson(content) : content}
+ {formatContent(type, content)}
{showCopy && (
-
+
-
+
)}
)}
{type === "image" && (
-
+
)}
{type === "audio" && (
)}
diff --git a/clients/web/src/components/atoms/InlineError/InlineError.tsx b/clients/web/src/components/atoms/InlineError/InlineError.tsx
index f4569f2b..5b232178 100644
--- a/clients/web/src/components/atoms/InlineError/InlineError.tsx
+++ b/clients/web/src/components/atoms/InlineError/InlineError.tsx
@@ -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 (
-
-
-
- {message}
-
+
+
+ {message}
{retryCount !== undefined && (
-
- Retry attempt {retryCount}
- {maxRetries !== undefined && ` of ${maxRetries}`}
-
+ {formatRetryLabel(retryCount, maxRetries)}
)}
-
+
{hasExpandable && (
-
+
)}
-
+
{details && {details}}
{docLink && (
-
+
)}
diff --git a/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx b/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx
index 1931ac9d..a29255a9 100644
--- a/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx
+++ b/clients/web/src/components/atoms/ListChangedIndicator/ListChangedIndicator.tsx
@@ -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 (
-
- List updated
-
-
+ List updated
+ ↻ Refresh
);
}
diff --git a/clients/web/src/components/atoms/LogEntry/LogEntry.tsx b/clients/web/src/components/atoms/LogEntry/LogEntry.tsx
index e6b5fc27..49b035fb 100644
--- a/clients/web/src/components/atoms/LogEntry/LogEntry.tsx
+++ b/clients/web/src/components/atoms/LogEntry/LogEntry.tsx
@@ -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 (
-
- {timestamp}
-
+ {timestamp}
{level}
- {logger && (
-
- [{logger}]
-
- )}
+ {logger && {formatLogger(logger)}}
{message}
diff --git a/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx b/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx
index 1d940599..ee24a4f1 100644
--- a/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx
+++ b/clients/web/src/components/atoms/MessageBubble/MessageBubble.tsx
@@ -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 (
-
+
-
- [{index}] role: {role}
-
+ {formatRoleLabel(index, role)}
- "{content}"
+ {formatQuotedContent(content)}
{imageContent && (
-
)}
{audioContent && (
)}
-
+
);
}
diff --git a/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx b/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx
index 5bcf279e..2783fd8e 100644
--- a/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx
+++ b/clients/web/src/components/atoms/ProgressDisplay/ProgressDisplay.tsx
@@ -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 (
- {description && (
-
- {description}
-
- )}
-
- {progress}%
-
+ {description && {description}}
+ {formatPercent(progress)}
- {elapsed && (
-
- {elapsed}
-
- )}
+ {elapsed && {elapsed}}
);
}
diff --git a/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx b/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx
index 0ceacc28..26c56078 100644
--- a/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx
+++ b/clients/web/src/components/atoms/StatusIndicator/StatusIndicator.tsx
@@ -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({