agent-ecosystem/src/renderer/components/runtime/ProviderModelBadges.tsx

142 lines
5.1 KiB
TypeScript

import { useState } from 'react';
import { cn } from '@renderer/lib/utils';
import {
getTeamModelBadgeLabel,
getVisibleTeamProviderModels,
} from '@renderer/utils/teamModelCatalog';
import { ChevronDown, ChevronUp } from 'lucide-react';
import type {
CliProviderId,
CliProviderModelAvailability,
CliProviderModelAvailabilityStatus,
CliProviderStatus,
} from '@shared/types';
function formatModelBadgeLabel(providerId: CliProviderId, model: string): string {
return getTeamModelBadgeLabel(providerId, model) ?? model;
}
function getAvailabilityStatus(
model: string,
modelAvailability: CliProviderModelAvailability[] | undefined
): CliProviderModelAvailabilityStatus | null {
return modelAvailability?.find((item) => item.modelId === model)?.status ?? null;
}
function getAvailabilityReason(
model: string,
modelAvailability: CliProviderModelAvailability[] | undefined
): string | null {
return modelAvailability?.find((item) => item.modelId === model)?.reason ?? null;
}
function getAvailabilityChip(status: CliProviderModelAvailabilityStatus | null): string | null {
switch (status) {
case 'checking':
return 'Checking';
case 'unavailable':
return 'Unavailable';
case 'unknown':
return 'Check failed';
case 'available':
default:
return null;
}
}
export const ProviderModelBadges = ({
providerId,
models,
modelAvailability,
providerStatus,
collapseAfter,
expandedMaxHeightPx = 200,
}: {
readonly providerId: CliProviderId;
readonly models: string[];
readonly modelAvailability?: CliProviderModelAvailability[];
readonly providerStatus?: Pick<CliProviderStatus, 'providerId' | 'authMethod' | 'backend'> | null;
readonly collapseAfter?: number;
readonly expandedMaxHeightPx?: number;
}): React.JSX.Element => {
const [expanded, setExpanded] = useState(false);
const visibleModels = getVisibleTeamProviderModels(providerId, models, providerStatus);
const displayModelAvailability = providerId === 'opencode' ? undefined : modelAvailability;
const shouldCollapse =
typeof collapseAfter === 'number' && collapseAfter > 0 && visibleModels.length > collapseAfter;
const displayedModels =
shouldCollapse && !expanded ? visibleModels.slice(0, collapseAfter) : visibleModels;
const hiddenCount = shouldCollapse ? visibleModels.length - collapseAfter : 0;
const badgeClassName =
'inline-flex items-center gap-1 rounded-md border px-1.5 py-px font-mono text-[10px] leading-4';
const badgeStyle = {
borderColor: 'var(--color-border-subtle)',
backgroundColor: 'rgba(255, 255, 255, 0.03)',
color: 'var(--color-text-secondary)',
};
const buttonClassName =
'inline-flex items-center gap-1 rounded-full border border-[rgba(59,130,246,0.35)] bg-[rgba(59,130,246,0.12)] px-2 py-px text-[10px] font-medium leading-4 text-[rgb(147,197,253)] transition-colors hover:border-[rgba(59,130,246,0.55)] hover:bg-[rgba(59,130,246,0.18)] hover:text-[rgb(191,219,254)]';
const listClassName = cn('flex flex-wrap gap-1.5', expanded && shouldCollapse ? 'pr-1' : null);
const listStyle =
expanded && shouldCollapse
? ({ maxHeight: expandedMaxHeightPx, overflowY: 'auto' } as const)
: undefined;
const renderModelBadge = (model: string, index: number): React.JSX.Element => {
const availabilityStatus = getAvailabilityStatus(model, displayModelAvailability);
const availabilityReason = getAvailabilityReason(model, displayModelAvailability);
const availabilityChip = getAvailabilityChip(availabilityStatus);
return (
<span
key={`${model}-${index}`}
className={badgeClassName}
style={badgeStyle}
title={availabilityReason ?? availabilityChip ?? undefined}
>
<span>{formatModelBadgeLabel(providerId, model)}</span>
{availabilityChip ? (
<span
className={cn(
'rounded px-1 py-0 text-[9px] font-medium uppercase tracking-[0.06em]',
availabilityStatus === 'checking'
? 'bg-[rgba(59,130,246,0.12)] text-[var(--color-text-secondary)]'
: availabilityStatus === 'unavailable'
? 'bg-[rgba(239,68,68,0.12)] text-[rgb(248,113,113)]'
: 'bg-[rgba(245,158,11,0.12)] text-[rgb(251,191,36)]'
)}
>
{availabilityChip}
</span>
) : null}
</span>
);
};
if (!shouldCollapse) {
return <div className="flex flex-wrap gap-1.5">{displayedModels.map(renderModelBadge)}</div>;
}
return (
<div className="flex flex-col items-start gap-1.5">
<div className={listClassName} style={listStyle}>
{displayedModels.map(renderModelBadge)}
{shouldCollapse && !expanded ? (
<button type="button" className={buttonClassName} onClick={() => setExpanded(true)}>
<ChevronDown className="size-3" />
<span>+{hiddenCount} more</span>
</button>
) : null}
</div>
{shouldCollapse && expanded ? (
<button type="button" className={buttonClassName} onClick={() => setExpanded(false)}>
<ChevronUp className="size-3" />
<span>Hide</span>
</button>
) : null}
</div>
);
};