mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
feat(filter.tsx): initial commit making filter component more generic - same style as user table filters
This commit is contained in:
parent
0f9ebc23a5
commit
b342f32f55
2 changed files with 72 additions and 208 deletions
|
|
@ -356,7 +356,8 @@
|
|||
"supports_vision": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"deprecation_date": "2025-07-14"
|
||||
},
|
||||
"gpt-4o-audio-preview": {
|
||||
"max_tokens": 16384,
|
||||
|
|
@ -1509,6 +1510,8 @@
|
|||
},
|
||||
"gpt-4o-transcribe": {
|
||||
"mode": "audio_transcription",
|
||||
"max_input_tokens": 16000,
|
||||
"max_output_tokens": 2000,
|
||||
"input_cost_per_token": 0.0000025,
|
||||
"input_cost_per_audio_token": 0.000006,
|
||||
"output_cost_per_token": 0.00001,
|
||||
|
|
@ -1517,6 +1520,8 @@
|
|||
},
|
||||
"gpt-4o-mini-transcribe": {
|
||||
"mode": "audio_transcription",
|
||||
"max_input_tokens": 16000,
|
||||
"max_output_tokens": 2000,
|
||||
"input_cost_per_token": 0.00000125,
|
||||
"input_cost_per_audio_token": 0.000003,
|
||||
"output_cost_per_token": 0.000005,
|
||||
|
|
@ -2439,7 +2444,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_vision": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"deprecation_date": "2025-08-20"
|
||||
},
|
||||
"azure/us/gpt-4o-2024-08-06": {
|
||||
"max_tokens": 16384,
|
||||
|
|
@ -2479,13 +2485,15 @@
|
|||
"max_output_tokens": 16384,
|
||||
"input_cost_per_token": 0.0000025,
|
||||
"output_cost_per_token": 0.000010,
|
||||
"cache_read_input_token_cost": 0.00000125,
|
||||
"litellm_provider": "azure",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_vision": true,
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"deprecation_date": "2025-12-20"
|
||||
},
|
||||
"azure/global-standard/gpt-4o-mini": {
|
||||
"max_tokens": 16384,
|
||||
|
|
@ -5349,14 +5357,14 @@
|
|||
"input_cost_per_image": 0,
|
||||
"input_cost_per_video_per_second": 0,
|
||||
"input_cost_per_audio_per_second": 0,
|
||||
"input_cost_per_token": 0,
|
||||
"input_cost_per_token": 0.00000015,
|
||||
"input_cost_per_character": 0,
|
||||
"input_cost_per_token_above_128k_tokens": 0,
|
||||
"input_cost_per_character_above_128k_tokens": 0,
|
||||
"input_cost_per_image_above_128k_tokens": 0,
|
||||
"input_cost_per_video_per_second_above_128k_tokens": 0,
|
||||
"input_cost_per_audio_per_second_above_128k_tokens": 0,
|
||||
"output_cost_per_token": 0,
|
||||
"output_cost_per_token": 0.0000006,
|
||||
"output_cost_per_character": 0,
|
||||
"output_cost_per_token_above_128k_tokens": 0,
|
||||
"output_cost_per_character_above_128k_tokens": 0,
|
||||
|
|
@ -5395,7 +5403,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supported_modalities": ["text", "image", "audio", "video"],
|
||||
"supported_output_modalities": ["text", "image"],
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"deprecation_date": "2026-02-05"
|
||||
},
|
||||
"gemini-2.0-flash-thinking-exp": {
|
||||
"max_tokens": 8192,
|
||||
|
|
@ -5599,7 +5608,8 @@
|
|||
"supported_modalities": ["text", "image", "audio", "video"],
|
||||
"supported_output_modalities": ["text"],
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash",
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"deprecation_date": "2026-02-25"
|
||||
},
|
||||
"gemini-2.5-pro-preview-03-25": {
|
||||
"max_tokens": 65536,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { Button, Input, Dropdown, MenuProps, Select, Spin } from 'antd';
|
||||
import { Button, Input, Dropdown, MenuProps, Select, Spin, Space } from 'antd';
|
||||
import { Card, Button as TremorButton } from '@tremor/react';
|
||||
import {
|
||||
FilterIcon,
|
||||
|
|
@ -38,121 +38,44 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
buttonLabel = "Filter",
|
||||
}) => {
|
||||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<string>(options[0]?.name || '');
|
||||
const [filterValues, setFilterValues] = useState<FilterValues>(initialValues);
|
||||
const [tempValues, setTempValues] = useState<FilterValues>(initialValues);
|
||||
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
|
||||
const [searchOptions, setSearchOptions] = useState<Array<{ label: string; value: string }>>([]);
|
||||
const [searchLoading, setSearchLoading] = useState<boolean>(false);
|
||||
const [searchInputValue, setSearchInputValue] = useState<string>('');
|
||||
const [searchOptionsMap, setSearchOptionsMap] = useState<{ [key: string]: Array<{ label: string; value: string }> }>({});
|
||||
const [searchLoadingMap, setSearchLoadingMap] = useState<{ [key: string]: boolean }>({});
|
||||
const [searchInputValueMap, setSearchInputValueMap] = useState<{ [key: string]: string }>({});
|
||||
|
||||
const filtersRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (filtersRef.current &&
|
||||
!filtersRef.current.contains(target) &&
|
||||
!target.closest('.ant-dropdown') &&
|
||||
!target.closest('.ant-select-dropdown')) {
|
||||
setShowFilters(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.length > 0 && options[0].isSearchable && options[0].searchFn) {
|
||||
loadInitialOptions(options[0]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadInitialOptions = async (option: FilterOption) => {
|
||||
if (!option.isSearchable || !option.searchFn) return;
|
||||
|
||||
setSearchLoading(true);
|
||||
try {
|
||||
const results = await option.searchFn('');
|
||||
setSearchOptions(results);
|
||||
} catch (error) {
|
||||
console.error('Error loading initial options:', error);
|
||||
setSearchOptions([]);
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showFilters && currentOption?.isSearchable && currentOption?.searchFn) {
|
||||
loadInitialOptions(currentOption);
|
||||
}
|
||||
}, [showFilters, selectedFilter]);
|
||||
|
||||
const handleFilterSelect = (key: string) => {
|
||||
setSelectedFilter(key);
|
||||
setDropdownOpen(false);
|
||||
|
||||
const newOption = options.find(opt => opt.name === key);
|
||||
if (newOption?.isSearchable && newOption?.searchFn) {
|
||||
loadInitialOptions(newOption);
|
||||
} else {
|
||||
setSearchOptions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSearch = useCallback(
|
||||
debounce(async (value: string, option: FilterOption) => {
|
||||
if (!option.isSearchable || !option.searchFn) return;
|
||||
|
||||
setSearchLoading(true);
|
||||
setSearchLoadingMap(prev => ({ ...prev, [option.name]: true }));
|
||||
try {
|
||||
const results = await option.searchFn(value);
|
||||
setSearchOptions(results);
|
||||
setSearchOptionsMap(prev => ({ ...prev, [option.name]: results }));
|
||||
} catch (error) {
|
||||
console.error('Error searching:', error);
|
||||
setSearchOptions([]);
|
||||
setSearchOptionsMap(prev => ({ ...prev, [option.name]: [] }));
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
setSearchLoadingMap(prev => ({ ...prev, [option.name]: false }));
|
||||
}
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleFilterChange = (value: string) => {
|
||||
|
||||
const handleFilterChange = (name: string, value: string) => {
|
||||
setTempValues(prev => ({
|
||||
...prev,
|
||||
[selectedFilter]: value
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
const emptyValues: FilterValues = {};
|
||||
options.forEach(option => {
|
||||
emptyValues[option.name] = '';
|
||||
});
|
||||
setTempValues(emptyValues);
|
||||
};
|
||||
|
||||
|
||||
const handleApplyFilters = () => {
|
||||
setFilterValues(tempValues);
|
||||
onApplyFilters(tempValues);
|
||||
setShowFilters(false);
|
||||
};
|
||||
|
||||
const dropdownItems: MenuProps['items'] = options.map(option => ({
|
||||
key: option.name,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedFilter === option.name && (
|
||||
<CheckIcon className="h-4 w-4 text-blue-600" />
|
||||
)}
|
||||
{option.label || option.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const currentOption = options.find(option => option.name === selectedFilter);
|
||||
|
||||
const resetFilters = () => {
|
||||
const emptyValues: FilterValues = {};
|
||||
|
|
@ -161,11 +84,11 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
});
|
||||
setTempValues(emptyValues);
|
||||
setFilterValues(emptyValues);
|
||||
setSearchInputValue('');
|
||||
setSearchOptions([]);
|
||||
onResetFilters(); // Call the parent's reset function
|
||||
setSearchInputValueMap({});
|
||||
setSearchOptionsMap({});
|
||||
onResetFilters();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative" ref={filtersRef}>
|
||||
<TremorButton
|
||||
|
|
@ -178,114 +101,45 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
|||
{buttonLabel}
|
||||
</TremorButton>
|
||||
{showFilters && (
|
||||
<Card className="absolute left-0 mt-2 w-[500px] z-50 border border-gray-200 shadow-lg">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Where</span>
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dropdownItems,
|
||||
onClick: ({ key }) => handleFilterSelect(key),
|
||||
style: { minWidth: '200px' }
|
||||
}}
|
||||
onOpenChange={setDropdownOpen}
|
||||
open={dropdownOpen}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button className="min-w-40 text-left flex justify-between items-center">
|
||||
{currentOption?.label || selectedFilter}
|
||||
{dropdownOpen ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
{currentOption?.isSearchable ? (
|
||||
<Select
|
||||
showSearch
|
||||
placeholder={`Search ${currentOption.label || selectedFilter}...`}
|
||||
value={tempValues[selectedFilter] || undefined}
|
||||
onChange={(value) => handleFilterChange(value)}
|
||||
onSearch={(value) => {
|
||||
setSearchInputValue(value);
|
||||
debouncedSearch(value, currentOption);
|
||||
}}
|
||||
onInputKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && searchInputValue) {
|
||||
// Allow manual entry of the value on Enter
|
||||
handleFilterChange(searchInputValue);
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
filterOption={false}
|
||||
className="flex-1 w-full max-w-full truncate min-w-100"
|
||||
loading={searchLoading}
|
||||
options={searchOptions}
|
||||
allowClear
|
||||
notFoundContent={
|
||||
searchLoading ? <Spin size="small" /> : (
|
||||
<div className="p-2">
|
||||
{searchInputValue && (
|
||||
<Button
|
||||
type="link"
|
||||
className="p-0 mt-1"
|
||||
onClick={() => {
|
||||
handleFilterChange(searchInputValue);
|
||||
// Close the dropdown/select after selecting the value
|
||||
const selectElement = document.activeElement as HTMLElement;
|
||||
if (selectElement) {
|
||||
selectElement.blur();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Use “{searchInputValue}” as filter value
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
placeholder="Enter value..."
|
||||
value={tempValues[selectedFilter] || ''}
|
||||
onChange={(e) => handleFilterChange(e.target.value)}
|
||||
className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
suffix={
|
||||
tempValues[selectedFilter] ? (
|
||||
<XIcon
|
||||
className="h-4 w-4 cursor-pointer text-gray-400 hover:text-gray-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleFilterChange('');
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
clearFilters();
|
||||
onResetFilters();
|
||||
setShowFilters(false);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={handleApplyFilters}>
|
||||
Apply Filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="mt-2">
|
||||
<Space size="middle" align="center">
|
||||
{options.map((option) => (
|
||||
<div key={option.name} className="flex items-center gap-2">
|
||||
{option.isSearchable ? (
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: 200 }}
|
||||
placeholder={`Search ${option.label || option.name}...`}
|
||||
value={tempValues[option.name] || undefined}
|
||||
onChange={(value) => handleFilterChange(option.name, value)}
|
||||
onSearch={(value) => {
|
||||
setSearchInputValueMap(prev => ({ ...prev, [option.name]: value }));
|
||||
if (option.searchFn) {
|
||||
debouncedSearch(value, option);
|
||||
}
|
||||
}}
|
||||
filterOption={false}
|
||||
loading={searchLoadingMap[option.name]}
|
||||
options={searchOptionsMap[option.name] || []}
|
||||
allowClear
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
style={{ width: 200 }}
|
||||
placeholder={`Enter ${option.label || option.name}...`}
|
||||
value={tempValues[option.name] || ''}
|
||||
onChange={(e) => handleFilterChange(option.name, e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Space>
|
||||
<Button onClick={resetFilters}>Reset</Button>
|
||||
<Button onClick={handleApplyFilters}>Apply</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue