Files
routstr-core/ui/components/model-provider-section.tsx

244 lines
8.3 KiB
TypeScript

import { useMemo } from 'react';
import type { Model } from '@/lib/api/schemas/models';
import type { AdminModelGroup } from '@/lib/api/services/admin';
import type { DisplayUnit } from '@/lib/types/units';
import { ModelItemCard } from '@/components/model-item-card';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
CheckSquare,
Edit3,
Globe,
Key,
MoreVertical,
RefreshCw,
} from 'lucide-react';
interface ModelProviderSectionProps {
provider: string;
providerModels: Model[];
displayUnit: DisplayUnit;
usdPerSat: number | null;
filterProvider?: string;
groupData?: AdminModelGroup;
selectedModels: Set<string>;
onSelectProviderModels: () => void;
onDeselectProviderModels: () => void;
onEditGroup: () => void;
onRefreshProviderModels: () => void;
onDeleteAllProviderModels: () => void;
onModelHover: (modelId: string | null) => void;
onModelToggleSelection: (modelId: string) => void;
onEditModel: (model: Model) => void;
onOverrideModel: (model: Model) => void;
onEnableModel: (modelId: string) => void;
onDisableModel: (modelId: string) => void;
onDeleteModel: (modelId: string) => void;
hasEffectiveApiKey: (model: Model) => boolean;
hasIndividualSettings: (model: Model) => boolean;
}
export function ModelProviderSection({
provider,
providerModels,
displayUnit,
usdPerSat,
filterProvider,
groupData,
selectedModels,
onSelectProviderModels,
onDeselectProviderModels,
onEditGroup,
onRefreshProviderModels,
onDeleteAllProviderModels,
onModelHover,
onModelToggleSelection,
onEditModel,
onOverrideModel,
onEnableModel,
onDisableModel,
onDeleteModel,
hasEffectiveApiKey,
hasIndividualSettings,
}: ModelProviderSectionProps) {
const allProviderSelected = providerModels.every((model) =>
selectedModels.has(model.id)
);
const someProviderSelected = providerModels.some((model) =>
selectedModels.has(model.id)
);
const keyedProviderModels = useMemo(() => {
const seenKeys = new Map<string, number>();
return providerModels.map((model) => {
const baseKey = [
provider,
model.id,
model.provider_id ?? '',
model.full_name,
model.api_key_type,
].join('::');
const seenCount = seenKeys.get(baseKey) ?? 0;
seenKeys.set(baseKey, seenCount + 1);
return {
model,
renderKey:
seenCount === 0 ? baseKey : `${baseKey}::duplicate-${seenCount}`,
};
});
}, [provider, providerModels]);
if (filterProvider) {
return (
<div className='bg-card/35 border-border/70 md:divide-border/75 overflow-hidden rounded-lg border md:divide-y'>
{keyedProviderModels.map(({ model, renderKey }) => (
<ModelItemCard
key={renderKey}
model={model}
displayUnit={displayUnit}
usdPerSat={usdPerSat}
isSelected={selectedModels.has(model.id)}
hasEffectiveApiKey={hasEffectiveApiKey(model)}
hasIndividualSettings={hasIndividualSettings(model)}
onHoverStart={() => onModelHover(model.id)}
onHoverEnd={() => onModelHover(null)}
onToggleSelection={() => onModelToggleSelection(model.id)}
onEdit={() => onEditModel(model)}
onOverride={() => onOverrideModel(model)}
onDisable={() => onDisableModel(model.id)}
onEnable={() => onEnableModel(model.id)}
onDelete={() => onDeleteModel(model.id)}
/>
))}
</div>
);
}
return (
<Card className='overflow-hidden'>
<CardHeader className='px-3 pb-2.5 sm:px-6 sm:pb-3'>
<div className='flex items-start justify-between gap-2 sm:gap-3'>
<div className='flex min-w-0 items-start gap-2.5 sm:gap-3'>
<Checkbox
checked={
allProviderSelected
? true
: someProviderSelected
? 'indeterminate'
: false
}
onCheckedChange={(checked) => {
if (checked === true) {
onSelectProviderModels();
return;
}
onDeselectProviderModels();
}}
className='border-border/90 data-checked:ring-primary/35 mt-0.5 size-4 data-checked:ring-2 sm:mt-1 sm:size-5'
aria-label={`Select models for provider ${provider}`}
/>
<div className='min-w-0'>
<CardTitle className='flex items-baseline gap-1.5 text-base sm:text-lg'>
<span className='truncate'>{provider}</span>
<span className='text-muted-foreground text-sm font-normal'>
({providerModels.length} models)
</span>
</CardTitle>
<CardDescription className='mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs sm:text-sm'>
{groupData?.group_url ? (
<span className='inline-flex items-center gap-1 break-all'>
<Globe className='h-3 w-3' />
{groupData.group_url}
</span>
) : (
'Using default endpoint'
)}
{groupData?.group_api_key && (
<span className='flex items-center gap-1'>
<Key className='h-3 w-3' />
Group API Key
</span>
)}
</CardDescription>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='icon'
className='h-7 w-7 sm:h-8 sm:w-8'
aria-label={`Provider actions for ${provider}`}
title={`Provider actions for ${provider}`}
>
<MoreVertical className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-64 sm:w-72'>
<DropdownMenuItem onClick={onEditGroup}>
<Edit3 className='mr-2 h-4 w-4' />
Edit Group
</DropdownMenuItem>
<DropdownMenuItem onClick={onSelectProviderModels}>
<CheckSquare className='mr-2 h-4 w-4' />
Select All
</DropdownMenuItem>
<DropdownMenuItem onClick={onRefreshProviderModels}>
<RefreshCw className='mr-2 h-4 w-4' />
Refresh Models
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDeleteAllProviderModels}
className='text-muted-foreground focus:text-foreground'
>
Delete all overrides
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className='px-3 pt-0 pb-3 sm:px-6 sm:pb-6'>
<div className='bg-card/35 border-border/70 md:divide-border/75 overflow-hidden rounded-lg border md:divide-y'>
{keyedProviderModels.map(({ model, renderKey }) => (
<ModelItemCard
key={renderKey}
model={model}
displayUnit={displayUnit}
usdPerSat={usdPerSat}
isSelected={selectedModels.has(model.id)}
hasEffectiveApiKey={hasEffectiveApiKey(model)}
hasIndividualSettings={hasIndividualSettings(model)}
onHoverStart={() => onModelHover(model.id)}
onHoverEnd={() => onModelHover(null)}
onToggleSelection={() => onModelToggleSelection(model.id)}
onEdit={() => onEditModel(model)}
onOverride={() => onOverrideModel(model)}
onDisable={() => onDisableModel(model.id)}
onEnable={() => onEnableModel(model.id)}
onDelete={() => onDeleteModel(model.id)}
/>
))}
</div>
</CardContent>
</Card>
);
}