From cf9a6d83ff27653b47dc29b71909e1c3f8e1d915 Mon Sep 17 00:00:00 2001 From: 9qeklajc <9qeklajc> Date: Fri, 24 Oct 2025 22:04:58 +0200 Subject: [PATCH] add model filtering --- ui/app/model/page.tsx | 12 ++- ui/components/ModelSearchFilter.tsx | 117 ++++++++++++++++++++++++++++ ui/components/ModelSelector.tsx | 77 +++++++++++++++--- 3 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 ui/components/ModelSearchFilter.tsx diff --git a/ui/app/model/page.tsx b/ui/app/model/page.tsx index 23f60c2a..afcda530 100644 --- a/ui/app/model/page.tsx +++ b/ui/app/model/page.tsx @@ -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([]); + const { data: modelsData, isLoading: isLoadingModels, @@ -142,7 +146,11 @@ export default function ModelsPage() {
Overview of all models across all provider groups.
- + + diff --git a/ui/components/ModelSearchFilter.tsx b/ui/components/ModelSearchFilter.tsx new file mode 100644 index 00000000..e88058a0 --- /dev/null +++ b/ui/components/ModelSearchFilter.tsx @@ -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('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 ( +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+ + + {hasActiveFilters && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/ui/components/ModelSelector.tsx b/ui/components/ModelSelector.tsx index 62fe0ea1..0c10ffd9 100644 --- a/ui/components/ModelSelector.tsx +++ b/ui/components/ModelSelector.tsx @@ -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(''); const [, setHoveredModelId] = useState(null); @@ -91,6 +94,9 @@ export function ModelSelector({ setBulkApplyGroupSettingsDialogOpen, ] = useState(false); + // Search and filter state + const [filteredModels, setFilteredModels] = useState([]); + 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>((acc, model) => { + return modelsToGroup.reduce>((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 (
- {/* Action buttons */}
{/* Model Management Actions {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({ {(() => { - 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 (