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" && ( - Content preview + )} {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({