Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/routers/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ def _collect_all_images(all_specs: list) -> list[dict]:
all_specs: List of Spec objects

Returns:
List of image dicts with spec_id, library, quality, url, thumb, and html
List of image dicts with spec_id, library, quality, url, thumb, html, and title
"""
all_images: list[dict] = []
for spec_obj in all_specs:
Expand All @@ -395,6 +395,7 @@ def _collect_all_images(all_specs: list) -> list[dict]:
"url": impl.preview_url,
"thumb": impl.preview_thumb,
"html": impl.preview_html,
"title": spec_obj.title,
}
)
return all_images
Expand Down Expand Up @@ -483,13 +484,17 @@ async def get_filtered_plots(request: Request, db: AsyncSession = Depends(requir
counts = _calculate_contextual_counts(filtered_images, spec_id_to_tags, impl_lookup)
or_counts = _calculate_or_counts(filter_groups, all_images, spec_id_to_tags, spec_lookup, impl_lookup)

# Build spec_id -> title mapping for search/tooltips
spec_titles = {spec_id: data["spec"].title for spec_id, data in spec_lookup.items() if data["spec"].title}

# Build and cache response
result = FilteredPlotsResponse(
total=len(filtered_images),
images=filtered_images,
counts=counts,
globalCounts=global_counts,
orCounts=or_counts,
specTitles=spec_titles,
)

try:
Expand Down
1 change: 1 addition & 0 deletions api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class FilteredPlotsResponse(BaseModel):
counts: dict
globalCounts: dict
orCounts: list[dict]
specTitles: dict[str, str] = {} # Mapping spec_id -> title for search/tooltips


class LibraryInfo(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7",
"fuse.js": "^7.1.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-helmet-async": "^2.0.5",
Expand Down
117 changes: 82 additions & 35 deletions app/src/components/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import { useTheme } from '@mui/material/styles';
import type { FilterCategory, ActiveFilters, FilterCounts } from '../types';
import { FILTER_LABELS, FILTER_TOOLTIPS, FILTER_CATEGORIES } from '../types';
import type { ImageSize } from '../constants';
import { getAvailableValues, getAvailableValuesForGroup, getSearchResults } from '../utils';
import { getAvailableValues, getAvailableValuesForGroup, getSearchResults, type SearchResult } from '../utils';

interface FilterBarProps {
activeFilters: ActiveFilters;
filterCounts: FilterCounts | null; // Contextual counts (for AND additions)
orCounts: Record<string, number>[]; // Per-group counts for OR additions
specTitles: Record<string, string>; // Mapping spec_id -> title for search/tooltips
currentTotal: number; // Total number of filtered images
displayedCount: number; // Currently displayed images
randomAnimation: { index: number; phase: 'out' | 'in'; oldLabel?: string } | null;
Expand All @@ -44,6 +45,7 @@ export function FilterBar({
activeFilters,
filterCounts,
orCounts,
specTitles,
currentTotal,
displayedCount,
randomAnimation,
Expand Down Expand Up @@ -208,8 +210,8 @@ export function FilterBar({

// Memoize search results to avoid recalculating on every render
const searchResults = useMemo(
() => getSearchResults(filterCounts, activeFilters, searchQuery, selectedCategory),
[filterCounts, activeFilters, searchQuery, selectedCategory]
() => getSearchResults(filterCounts, activeFilters, searchQuery, selectedCategory, specTitles),
[filterCounts, activeFilters, searchQuery, selectedCategory, specTitles]
);

// Track searches with no results (debounced, to discover missing specs)
Expand Down Expand Up @@ -699,38 +701,83 @@ export function FilterBar({
]
: []),
...(searchResults.length > 0
? searchResults.map(({ category, value, count }, idx) => (
<MenuItem
key={`${category}-${value}`}
onClick={() => handleValueSelect(category, value)}
selected={idx === highlightedIndex}
sx={{ fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace' }}
>
<ListItemText
primary={value}
secondary={!selectedCategory ? FILTER_LABELS[category] : undefined}
primaryTypographyProps={{
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
fontSize: '0.85rem',
}}
secondaryTypographyProps={{
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
fontSize: '0.7rem',
color: '#9ca3af',
}}
/>
<Typography
sx={{
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
fontSize: '0.75rem',
color: '#9ca3af',
ml: 2,
}}
>
({count})
</Typography>
</MenuItem>
))
? (() => {
// Split results into exact and fuzzy matches
const exactResults = searchResults.filter((r) => r.matchType === 'exact');
const fuzzyResults = searchResults.filter((r) => r.matchType === 'fuzzy');

const renderMenuItem = (result: SearchResult, idx: number) => {
const { category, value, count } = result;
const specTitle = category === 'spec' ? specTitles[value] : undefined;
const menuItem = (
<MenuItem
key={`${category}-${value}`}
onClick={() => handleValueSelect(category, value)}
selected={idx === highlightedIndex}
sx={{ fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace' }}
>
<ListItemText
primary={value}
secondary={!selectedCategory ? FILTER_LABELS[category] : undefined}
primaryTypographyProps={{
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
fontSize: '0.85rem',
}}
secondaryTypographyProps={{
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
fontSize: '0.7rem',
color: '#9ca3af',
}}
/>
<Typography
sx={{
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
fontSize: '0.75rem',
color: '#9ca3af',
ml: 2,
}}
>
({count})
</Typography>
</MenuItem>
);
return specTitle ? (
<Tooltip key={`${category}-${value}`} title={specTitle} placement="right" arrow>
<span>{menuItem}</span>
</Tooltip>
) : (
menuItem
);
Comment on lines +744 to +750
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping MenuItem in a <span> is necessary for Tooltip to work, but this duplicates the key attribute (it's already on menuItem at line 713). The wrapping span should not have a key prop. Remove the key from the ternary expression at line 745 since the menuItem already has the unique key.

Copilot uses AI. Check for mistakes.
};

const items: React.ReactNode[] = [];
// Add exact matches
exactResults.forEach((result, i) => {
items.push(renderMenuItem(result, i));
});
// Add fuzzy label/divider if there are fuzzy results
if (fuzzyResults.length > 0) {
items.push(
<Divider key="exact-fuzzy-divider" sx={{ my: 0.5 }}>
<Typography
sx={{
fontSize: '0.65rem',
color: '#9ca3af',
fontFamily: '"MonoLisa", "MonoLisa Fallback", monospace',
px: 1,
}}
>
fuzzy
</Typography>
</Divider>
);
}
// Add fuzzy matches
fuzzyResults.forEach((result, i) => {
items.push(renderMenuItem(result, exactResults.length + i));
});
return items;
})()
: [
<MenuItem key="no-results" disabled>
<Typography
Expand Down
6 changes: 5 additions & 1 deletion app/src/hooks/useFilterFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface FilterFetchState {
filterCounts: FilterCounts | null;
globalCounts: FilterCounts | null;
orCounts: Record<string, number>[];
specTitles: Record<string, string>;
allImages: PlotImage[];
displayedImages: PlotImage[];
hasMore: boolean;
Expand Down Expand Up @@ -55,6 +56,7 @@ export function useFilterFetch({
const [filterCounts, setFilterCounts] = useState<FilterCounts | null>(initialState.filterCounts ?? null);
const [globalCounts, setGlobalCounts] = useState<FilterCounts | null>(initialState.globalCounts ?? null);
const [orCounts, setOrCounts] = useState<Record<string, number>[]>(initialState.orCounts ?? []);
const [specTitles, setSpecTitles] = useState<Record<string, string>>(initialState.specTitles ?? {});
const [allImages, setAllImages] = useState<PlotImage[]>(initialState.allImages ?? []);
const [displayedImages, setDisplayedImages] = useState<PlotImage[]>(initialState.displayedImages ?? []);
const [hasMore, setHasMore] = useState(initialState.hasMore ?? false);
Expand Down Expand Up @@ -97,10 +99,11 @@ export function useFilterFetch({

if (abortController.signal.aborted) return;

// Update filter counts
// Update filter counts and spec titles
setFilterCounts(data.counts);
setGlobalCounts(data.globalCounts || data.counts);
setOrCounts(data.orCounts || []);
setSpecTitles(data.specTitles || {});

// Shuffle images randomly on each load
const shuffled = shuffleArray<PlotImage>(data.images || []);
Expand Down Expand Up @@ -128,6 +131,7 @@ export function useFilterFetch({
filterCounts,
globalCounts,
orCounts,
specTitles,
allImages,
displayedImages,
hasMore,
Expand Down
3 changes: 3 additions & 0 deletions app/src/hooks/useFilterState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface UseFilterStateReturn {
filterCounts: FilterCounts | null;
globalCounts: FilterCounts | null;
orCounts: Record<string, number>[];
specTitles: Record<string, string>;
allImages: PlotImage[];
displayedImages: PlotImage[];
hasMore: boolean;
Expand Down Expand Up @@ -86,6 +87,7 @@ export function useFilterState({
filterCounts,
globalCounts,
orCounts,
specTitles,
allImages,
displayedImages,
hasMore,
Expand Down Expand Up @@ -234,6 +236,7 @@ export function useFilterState({
filterCounts,
globalCounts,
orCounts,
specTitles,
allImages,
displayedImages,
hasMore,
Expand Down
2 changes: 2 additions & 0 deletions app/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function HomePage() {
activeFilters,
filterCounts,
orCounts,
specTitles,
allImages,
displayedImages,
hasMore,
Expand Down Expand Up @@ -167,6 +168,7 @@ export function HomePage() {
activeFilters={activeFilters}
filterCounts={filterCounts}
orCounts={orCounts}
specTitles={specTitles}
currentTotal={allImages.length}
displayedCount={displayedImages.length}
randomAnimation={randomAnimation}
Expand Down
2 changes: 2 additions & 0 deletions app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface PlotImage {
html?: string;
code?: string;
spec_id?: string;
title?: string;
}

// Filter system types
Expand Down Expand Up @@ -95,6 +96,7 @@ export interface FilteredPlotsResponse {
counts: FilterCounts; // Contextual counts (for AND additions)
globalCounts: FilterCounts; // Global counts (for reference)
orCounts: Record<string, number>[]; // Per-group counts for OR additions
specTitles: Record<string, string>; // Mapping spec_id -> title for search/tooltips
}

export interface LibraryInfo {
Expand Down
Loading
Loading