feat(filter.tsx): initial commit making filter component more generic - same style as user table filters

This commit is contained in:
Krrish Dholakia 2025-04-25 11:22:55 -07:00
parent 0f9ebc23a5
commit b342f32f55
2 changed files with 72 additions and 208 deletions

View file

@ -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,

View file

@ -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 &ldquo;{searchInputValue}&rdquo; 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>
);