add model filtering

This commit is contained in:
9qeklajc
2025-10-24 22:04:58 +02:00
parent 82e01e3e13
commit cf9a6d83ff
3 changed files with 192 additions and 14 deletions
+10 -2
View File
@@ -3,6 +3,7 @@
import { ModelSelector } from '@/components/ModelSelector';
import { ModelTester } from '@/components/ModelTester';
import { ApiEndpointTester } from '@/components/ApiEndpointTester';
import { ModelSearchFilter } from '@/components/ModelSearchFilter';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
import { SiteHeader } from '@/components/site-header';
@@ -13,9 +14,12 @@ import { Skeleton } from '@/components/ui/skeleton';
import { AlertCircle, Users, Globe } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import type { Model } from '@/lib/api/schemas/models';
export default function ModelsPage() {
const [filteredModels, setFilteredModels] = useState<Model[]>([]);
const {
data: modelsData,
isLoading: isLoadingModels,
@@ -142,7 +146,11 @@ export default function ModelsPage() {
<div className='text-muted-foreground text-sm'>
Overview of all models across all provider groups.
</div>
<ModelSelector />
<ModelSearchFilter
models={models}
onFilteredModelsChange={setFilteredModels}
/>
<ModelSelector filteredModels={filteredModels} />
</div>
</TabsContent>
+117
View File
@@ -0,0 +1,117 @@
'use client';
import React, { useState, useMemo } from 'react';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { Search, Filter, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Model } from '@/lib/api/schemas/models';
interface ModelSearchFilterProps {
models: Model[];
onFilteredModelsChange: (filteredModels: Model[]) => void;
className?: string;
}
type SortOption = 'name-asc' | 'name-desc' | 'price-asc' | 'price-desc';
export function ModelSearchFilter({ models, onFilteredModelsChange, className }: ModelSearchFilterProps) {
const [searchQuery, setSearchQuery] = useState('');
const [sortOption, setSortOption] = useState<SortOption>('name-asc');
const filteredAndSortedModels = useMemo(() => {
let filtered = [...models];
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
filtered = filtered.filter((model) => {
return (
model.name.toLowerCase().includes(query) ||
model.full_name.toLowerCase().includes(query) ||
model.provider.toLowerCase().includes(query) ||
(model.description && model.description.toLowerCase().includes(query)) ||
model.modelType.toLowerCase().includes(query)
);
});
}
// Apply sorting
filtered.sort((a, b) => {
switch (sortOption) {
case 'name-asc':
return a.name.localeCompare(b.name);
case 'name-desc':
return b.name.localeCompare(a.name);
case 'price-asc': {
const aPrice = a.is_free ? 0 : (a.input_cost || 0) + (a.output_cost || 0);
const bPrice = b.is_free ? 0 : (b.input_cost || 0) + (b.output_cost || 0);
return aPrice - bPrice;
}
case 'price-desc': {
const aPrice = a.is_free ? 0 : (a.input_cost || 0) + (a.output_cost || 0);
const bPrice = b.is_free ? 0 : (b.input_cost || 0) + (b.output_cost || 0);
return bPrice - aPrice;
}
default:
return 0;
}
});
return filtered;
}, [models, searchQuery, sortOption]);
// Notify parent component when filtered models change
React.useEffect(() => {
onFilteredModelsChange(filteredAndSortedModels);
}, [filteredAndSortedModels, onFilteredModelsChange]);
const clearFilters = () => {
setSearchQuery('');
setSortOption('name-asc');
};
const hasActiveFilters = searchQuery.trim() !== '' || sortOption !== 'name-asc';
return (
<div className={cn('flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-3', className)}>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search models by name, provider, or description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex items-center gap-2">
<Select value={sortOption} onValueChange={(value: SortOption) => setSortOption(value)}>
<SelectTrigger className="w-full sm:w-[180px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="price-asc">Price (Low to High)</SelectItem>
<SelectItem value="price-desc">Price (High to Low)</SelectItem>
</SelectContent>
</Select>
{hasActiveFilters && (
<Button
variant="outline"
size="icon"
onClick={clearFilters}
className="h-9 w-9"
title="Clear filters"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
</div>
);
}
+65 -12
View File
@@ -13,6 +13,7 @@ import { AddModelForm } from '@/components/AddModelForm';
import { EditModelForm } from '@/components/EditModelForm';
import { EditGroupForm } from '@/components/EditGroupForm';
import { CollectModelsDialog } from '@/components/CollectModelsDialog';
import { ModelSearchFilter } from '@/components/ModelSearchFilter';
import { formatCost } from '@/lib/services/costValidation';
import { Button } from '@/components/ui/button';
import {
@@ -64,12 +65,14 @@ interface ModelSelectorProps {
filterProvider?: string;
groupData?: ModelGroup;
showProviderActions?: boolean;
filteredModels?: Model[];
}
export function ModelSelector({
filterProvider,
groupData,
showProviderActions = false,
filteredModels: propFilteredModels,
}: ModelSelectorProps) {
const [selectedModelId, setSelectedModelId] = useState<string>('');
const [, setHoveredModelId] = useState<string | null>(null);
@@ -91,6 +94,9 @@ export function ModelSelector({
setBulkApplyGroupSettingsDialogOpen,
] = useState(false);
// Search and filter state
const [filteredModels, setFilteredModels] = useState<Model[]>([]);
const queryClient = useQueryClient();
// Fetch models and groups
@@ -108,7 +114,7 @@ export function ModelSelector({
const { models = [], groups = [] } = modelsData || {};
// Filter models by provider if specified
const filteredModels = useMemo(() => {
const providerFilteredModels = useMemo(() => {
if (!filterProvider) return models;
return models.filter((model) => model.provider === filterProvider);
}, [models, filterProvider]);
@@ -418,14 +424,21 @@ export function ModelSelector({
// Group models by provider for better organization (only if not filtering)
const groupedModels = useMemo(() => {
const modelsToGroup =
propFilteredModels && propFilteredModels.length > 0
? propFilteredModels
: filteredModels.length > 0
? filteredModels
: providerFilteredModels;
if (filterProvider) {
// If filtering by provider, return single group
return { [filterProvider]: filteredModels };
return { [filterProvider]: modelsToGroup };
}
if (!models) return {};
if (!modelsToGroup) return {};
return models.reduce<Record<string, Model[]>>((acc, model) => {
return modelsToGroup.reduce<Record<string, Model[]>>((acc, model) => {
const provider = model.provider;
if (!acc[provider]) {
acc[provider] = [];
@@ -433,7 +446,12 @@ export function ModelSelector({
acc[provider].push(model);
return acc;
}, {});
}, [models, filteredModels, filterProvider]);
}, [
providerFilteredModels,
filteredModels,
propFilteredModels,
filterProvider,
]);
// Create a map of provider names to group data
const groupDataMap = useMemo(() => {
@@ -622,7 +640,13 @@ export function ModelSelector({
};
const selectAllModels = () => {
setSelectedModels(new Set(filteredModels.map((m) => m.id)));
const modelsToSelect =
propFilteredModels && propFilteredModels.length > 0
? propFilteredModels
: filteredModels.length > 0
? filteredModels
: providerFilteredModels;
setSelectedModels(new Set(modelsToSelect.map((m) => m.id)));
};
const deselectAllModels = () => {
@@ -630,7 +654,13 @@ export function ModelSelector({
};
const selectProviderModels = (provider: string) => {
const providerModelIds = filteredModels
const modelsToFilter =
propFilteredModels && propFilteredModels.length > 0
? propFilteredModels
: filteredModels.length > 0
? filteredModels
: providerFilteredModels;
const providerModelIds = modelsToFilter
.filter((m) => m.provider === provider)
.map((m) => m.id);
const newSelected = new Set(selectedModels);
@@ -795,7 +825,6 @@ export function ModelSelector({
return (
<div className='grid gap-6'>
{/* Action buttons */}
<div className='flex flex-wrap items-center gap-2'>
{/* Model Management Actions
<Button onClick={() => setIsAddFormOpen(true)} className='gap-2'>
@@ -817,7 +846,13 @@ export function ModelSelector({
<Button
onClick={() => {
const allModelIds = filteredModels.map((m) => m.id);
const modelsToSelect =
propFilteredModels && propFilteredModels.length > 0
? propFilteredModels
: filteredModels.length > 0
? filteredModels
: providerFilteredModels;
const allModelIds = modelsToSelect.map((m) => m.id);
const newSelected = new Set(selectedModels);
allModelIds.forEach((id) => newSelected.add(id));
setSelectedModels(newSelected);
@@ -827,7 +862,13 @@ export function ModelSelector({
className='gap-2'
>
<CheckSquare className='h-4 w-4' />
Select All ({filteredModels.length})
Select All (
{propFilteredModels && propFilteredModels.length > 0
? propFilteredModels.length
: filteredModels.length > 0
? filteredModels.length
: providerFilteredModels.length}
)
</Button>
{selectedModels.size > 0 && (
@@ -835,7 +876,13 @@ export function ModelSelector({
onClick={() => {
if (filterProvider) {
// If in provider view, deselect only models from this provider
const groupModelIds = filteredModels.map((m) => m.id);
const modelsToFilter =
propFilteredModels && propFilteredModels.length > 0
? propFilteredModels
: filteredModels.length > 0
? filteredModels
: providerFilteredModels;
const groupModelIds = modelsToFilter.map((m) => m.id);
const newSelected = new Set(selectedModels);
groupModelIds.forEach((id) => newSelected.delete(id));
setSelectedModels(newSelected);
@@ -864,7 +911,13 @@ export function ModelSelector({
</span>
{(() => {
const selectedSoftDeletedModels = filteredModels.filter(
const modelsToFilter =
propFilteredModels && propFilteredModels.length > 0
? propFilteredModels
: filteredModels.length > 0
? filteredModels
: providerFilteredModels;
const selectedSoftDeletedModels = modelsToFilter.filter(
(m) => selectedModels.has(m.id) && m.soft_deleted
);
return (