feat(model-cost-info): show compact price and capability icons in ModelControls (#425)
* feat: add compact price display and capability slides in ModelControls Show compact price text on desktop in ModelControls. Render slides for price and capability icons in a rotating metadata section. Rotate metadata only on non-VSCode runtimes. * feat: prefer capabilities over price in static runtimes for ModelControls
This commit is contained in:
@@ -37,6 +37,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TextLoop } from '@/components/ui/TextLoop';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useIsVSCodeRuntime } from '@/hooks/useRuntimeAPIs';
|
||||
import { isDesktopShell } from '@/lib/desktop';
|
||||
@@ -207,6 +208,28 @@ const formatCost = (value?: number | null) => {
|
||||
return CURRENCY_FORMATTER.format(value);
|
||||
};
|
||||
|
||||
const formatCompactPrice = (metadata?: ModelMetadata): string | null => {
|
||||
if (!metadata?.cost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputCost = metadata.cost.input;
|
||||
const outputCost = metadata.cost.output;
|
||||
const hasInput = typeof inputCost === 'number' && Number.isFinite(inputCost);
|
||||
const hasOutput = typeof outputCost === 'number' && Number.isFinite(outputCost);
|
||||
|
||||
if (hasInput && hasOutput) {
|
||||
return `In ${formatCost(inputCost)} · Out ${formatCost(outputCost)}`;
|
||||
}
|
||||
if (hasInput) {
|
||||
return `In ${formatCost(inputCost)}`;
|
||||
}
|
||||
if (hasOutput) {
|
||||
return `Out ${formatCost(outputCost)}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getCapabilityIcons = (metadata?: ModelMetadata) => {
|
||||
return CAPABILITY_DEFINITIONS.filter((definition) => definition.isActive(metadata)).map((definition) => ({
|
||||
key: definition.key,
|
||||
@@ -1936,6 +1959,44 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
const showProviderLogo = keyPrefix === 'fav' || keyPrefix === 'recent';
|
||||
|
||||
// Build animated metadata slides for desktop
|
||||
const priceText = formatCompactPrice(metadata);
|
||||
const hasPrice = priceText !== null;
|
||||
const hasCapabilities = indicatorIcons.length > 0;
|
||||
|
||||
// Build slides array: price first, then capabilities
|
||||
const slides: React.ReactNode[] = [];
|
||||
if (hasPrice) {
|
||||
slides.push(
|
||||
<span key="price" className="typography-micro text-muted-foreground whitespace-nowrap">
|
||||
{priceText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (hasCapabilities) {
|
||||
slides.push(
|
||||
<div key="capabilities" className="flex items-center gap-0.5">
|
||||
{indicatorIcons.map(({ id, icon: Icon, label }) => (
|
||||
<span
|
||||
key={id}
|
||||
className="flex h-3.5 w-3.5 items-center justify-center text-muted-foreground"
|
||||
aria-label={label}
|
||||
role="img"
|
||||
title={label}
|
||||
>
|
||||
<Icon className="h-2.5 w-2.5" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Rotate metadata in interactive desktop-style pickers (web/desktop), keep VS Code static.
|
||||
const supportsRotatingMetadata = !isVSCodeRuntime;
|
||||
const shouldAnimate = supportsRotatingMetadata && slides.length > 1 && (isHighlighted || isSelected);
|
||||
const staticSlideIndex = !supportsRotatingMetadata && hasCapabilities && hasPrice ? 1 : 0;
|
||||
const staticMetadataSlide = slides[staticSlideIndex];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${keyPrefix}-${providerID}-${modelID}`}
|
||||
@@ -1961,19 +2022,22 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{indicatorIcons.length > 0 && (
|
||||
<div className={cn("items-center gap-0.5", isHighlighted ? "flex" : "hidden group-hover:flex")}>
|
||||
{indicatorIcons.map(({ id, icon: Icon, label }) => (
|
||||
<span
|
||||
key={id}
|
||||
className="flex h-3.5 w-3.5 items-center justify-center text-muted-foreground"
|
||||
aria-label={label}
|
||||
role="img"
|
||||
title={label}
|
||||
>
|
||||
<Icon className="h-2.5 w-2.5" />
|
||||
</span>
|
||||
))}
|
||||
{/* Metadata slot: animated TextLoop for desktop highlighted/selected rows, static otherwise */}
|
||||
{slides.length > 0 && (
|
||||
<div className={cn(
|
||||
"items-center",
|
||||
shouldAnimate ? "flex w-[140px] justify-end" : ((isHighlighted || isSelected) ? "flex" : "hidden group-hover:flex")
|
||||
)}>
|
||||
{shouldAnimate ? (
|
||||
<TextLoop interval={2.1} transition={{ duration: 0.25 }} trigger={shouldAnimate}>
|
||||
{slides}
|
||||
</TextLoop>
|
||||
) : (
|
||||
<>
|
||||
{/* In static runtimes (VS Code), prefer capabilities over price when both exist. */}
|
||||
{staticMetadataSlide}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isSelected && (
|
||||
|
||||
Reference in New Issue
Block a user