From d307fffc102305a3c2cb60cedc9be0024c486eca Mon Sep 17 00:00:00 2001 From: ibelick Date: Sun, 8 Feb 2026 11:41:58 +0100 Subject: [PATCH] feat: add preview-card, improve context-meter UI --- .../src/components/ui/preview-card.tsx | 64 +++++++++++++ .../screens/chat/components/chat-header.tsx | 8 +- .../screens/chat/components/context-meter.tsx | 93 +++++++++++-------- 3 files changed, 122 insertions(+), 43 deletions(-) create mode 100644 apps/webclaw/src/components/ui/preview-card.tsx diff --git a/apps/webclaw/src/components/ui/preview-card.tsx b/apps/webclaw/src/components/ui/preview-card.tsx new file mode 100644 index 0000000..287156d --- /dev/null +++ b/apps/webclaw/src/components/ui/preview-card.tsx @@ -0,0 +1,64 @@ +'use client' + +import { PreviewCard as PreviewCardPrimitive } from '@base-ui/react/preview-card' +import { cn } from '@/lib/utils' + +const PreviewCard = PreviewCardPrimitive.Root + +type PreviewCardTriggerProps = React.ComponentProps< + typeof PreviewCardPrimitive.Trigger +> + +function PreviewCardTrigger({ className, ...props }: PreviewCardTriggerProps) { + return ( + + ) +} + +type PreviewCardPopupProps = PreviewCardPrimitive.Popup.Props & { + align?: PreviewCardPrimitive.Positioner.Props['align'] + sideOffset?: PreviewCardPrimitive.Positioner.Props['sideOffset'] +} + +function PreviewCardPopup({ + className, + children, + align = 'center', + sideOffset = 6, + ...props +}: PreviewCardPopupProps) { + return ( + + + + {children} + + + + ) +} + +export { + PreviewCard, + PreviewCard as HoverCard, + PreviewCardTrigger, + PreviewCardTrigger as HoverCardTrigger, + PreviewCardPopup, + PreviewCardPopup as HoverCardContent, +} diff --git a/apps/webclaw/src/screens/chat/components/chat-header.tsx b/apps/webclaw/src/screens/chat/components/chat-header.tsx index acea181..c0a64ca 100644 --- a/apps/webclaw/src/screens/chat/components/chat-header.tsx +++ b/apps/webclaw/src/screens/chat/components/chat-header.tsx @@ -24,7 +24,7 @@ function ChatHeaderComponent({ return (
{showSidebarButton ? (
) } diff --git a/apps/webclaw/src/screens/chat/components/context-meter.tsx b/apps/webclaw/src/screens/chat/components/context-meter.tsx index ffebc1b..6125bc4 100644 --- a/apps/webclaw/src/screens/chat/components/context-meter.tsx +++ b/apps/webclaw/src/screens/chat/components/context-meter.tsx @@ -1,58 +1,77 @@ import { memo, useMemo } from 'react' -import { cn } from '@/lib/utils' +import { + PreviewCard, + PreviewCardPopup, + PreviewCardTrigger, +} from '@/components/ui/preview-card' type ContextMeterProps = { usedTokens?: number maxTokens?: number - className?: string } -function ContextMeterComponent({ - usedTokens, - maxTokens, - className, -}: ContextMeterProps) { - const { percentage, color, label } = useMemo(() => { - if (!usedTokens || !maxTokens) return { percentage: 0, color: '', label: '' } +function ContextMeterComponent({ usedTokens, maxTokens }: ContextMeterProps) { + const { percentage, usedLabel, leftPercentage } = useMemo(() => { + if (!usedTokens || !maxTokens) + return { + percentage: 0, + usedLabel: '', + leftPercentage: 0, + } const pct = Math.min((usedTokens / maxTokens) * 100, 100) const fmt = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(0)}K` : String(n) return { percentage: pct, - color: - pct >= 90 - ? 'bg-red-500' - : pct >= 70 - ? 'bg-yellow-500' - : 'bg-green-500', - label: `${fmt(usedTokens)} / ${fmt(maxTokens)} tokens (${pct.toFixed(0)}%)`, + usedLabel: `${fmt(usedTokens)} / ${fmt(maxTokens)} tokens used`, + leftPercentage: Math.max(0, 100 - pct), } }, [usedTokens, maxTokens]) if (!usedTokens || !maxTokens || percentage === 0) return null return ( -
-
-
= 95 && 'animate-pulse', - )} - style={{ width: `${percentage}%` }} - /> -
- - {percentage.toFixed(0)}% - -
+ + +
+ +
+
+ +
+
Context window:
+
+ {percentage.toFixed(0)}% used ({leftPercentage.toFixed(0)}% left) +
+
{usedLabel}
+
+
+
) }