feat: add preview-card, improve context-meter UI

This commit is contained in:
ibelick
2026-02-08 11:41:58 +01:00
parent ffd5909d1e
commit d307fffc10
3 changed files with 122 additions and 43 deletions
@@ -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 (
<PreviewCardPrimitive.Trigger
className={cn(className)}
data-slot="preview-card-trigger"
{...props}
/>
)
}
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 (
<PreviewCardPrimitive.Portal>
<PreviewCardPrimitive.Positioner
align={align}
className="z-50"
data-slot="preview-card-positioner"
sideOffset={sideOffset}
>
<PreviewCardPrimitive.Popup
className={cn(
'relative w-64 origin-(--transform-origin) rounded-lg bg-primary-50 p-3 text-sm text-pretty text-primary-900 outline outline-primary-950/10 shadow-2xs',
className,
)}
data-slot="preview-card-content"
{...props}
>
{children}
</PreviewCardPrimitive.Popup>
</PreviewCardPrimitive.Positioner>
</PreviewCardPrimitive.Portal>
)
}
export {
PreviewCard,
PreviewCard as HoverCard,
PreviewCardTrigger,
PreviewCardTrigger as HoverCardTrigger,
PreviewCardPopup,
PreviewCardPopup as HoverCardContent,
}
@@ -24,7 +24,7 @@ function ChatHeaderComponent({
return (
<div
ref={wrapperRef}
className="border-b border-primary-200 px-4 h-12 flex items-center bg-surface"
className="border-b border-primary-200 px-4 h-12 flex items-center bg-surface justify-between"
>
{showSidebarButton ? (
<Button
@@ -38,11 +38,7 @@ function ChatHeaderComponent({
</Button>
) : null}
<div className="text-sm font-medium truncate">{activeTitle}</div>
<ContextMeter
usedTokens={usedTokens}
maxTokens={maxTokens}
className="ml-3 hidden sm:flex"
/>
<ContextMeter usedTokens={usedTokens} maxTokens={maxTokens} />
</div>
)
}
@@ -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 (
<div
className={cn(
'flex items-center gap-2 text-xs text-primary-500',
className,
)}
title={label}
>
<div className="w-20 h-1.5 bg-primary-100 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
color,
percentage >= 95 && 'animate-pulse',
)}
style={{ width: `${percentage}%` }}
/>
</div>
<span className="tabular-nums whitespace-nowrap">
{percentage.toFixed(0)}%
</span>
</div>
<PreviewCard>
<PreviewCardTrigger className="flex items-center gap-2 text-xs text-primary-500">
<div className="size-4 text-primary-200">
<svg
viewBox="0 0 36 36"
className="size-4 -rotate-90"
aria-hidden="true"
>
<circle
cx="18"
cy="18"
r="15.5"
fill="none"
stroke="currentColor"
strokeWidth="4"
className="text-primary-300"
/>
<circle
cx="18"
cy="18"
r="15.5"
fill="none"
stroke="currentColor"
className="text-primary-600"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray={`${(percentage / 100) * 97.4} 97.4`}
/>
</svg>
</div>
</PreviewCardTrigger>
<PreviewCardPopup align="end" sideOffset={8} className="w-52 px-2 py-1">
<div className="space-y-0.5 text-xs text-primary-900">
<div className="text-primary-950 font-[450]">Context window:</div>
<div className="tabular-nums text-primary-700">
{percentage.toFixed(0)}% used ({leftPercentage.toFixed(0)}% left)
</div>
<div className="tabular-nums text-primary-700">{usedLabel}</div>
</div>
</PreviewCardPopup>
</PreviewCard>
)
}