mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-06 12:38:23 +00:00
add model filtering
This commit is contained in:
+10
-2
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user