diff --git a/components/Settings/SettingsModal.tsx b/components/Settings/SettingsModal.tsx index e7917dd..e5b0929 100644 --- a/components/Settings/SettingsModal.tsx +++ b/components/Settings/SettingsModal.tsx @@ -1,8 +1,6 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { IconAlertCircle, - IconChevronDown, - IconChevronRight, IconDownload, IconPalette, IconRefresh, @@ -13,19 +11,19 @@ import { IconX, } from '@tabler/icons-react'; import { + Accordion, ActionIcon, Alert, Badge, - Box, Button, Card, - Collapse, ColorSwatch, Divider, Group, Loader, Modal, NavLink, + Pagination, PasswordInput, rem, ScrollArea, @@ -36,6 +34,7 @@ import { Tooltip, useMantineTheme, } from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama'; // Type for the scraped models JSON @@ -89,24 +88,45 @@ export function SettingsModal({ const [pullingModel, setPullingModel] = useState(null); const [pullError, setPullError] = useState(''); - // Expanded model card state - const [expandedModel, setExpandedModel] = useState(null); - - // Search state for available models + // Search state for available models (with debounce for performance) const [modelSearch, setModelSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(modelSearch, 200); + + // Pagination state + const MODELS_PER_PAGE = 20; + const [currentPage, setCurrentPage] = useState(1); // Track if we've fetched this session const hasFetchedInstalled = useRef(false); const hasFetchedAvailable = useRef(false); // Get list of model names sorted alphabetically - const modelNames = availableModels ? Object.keys(availableModels.models).sort() : []; - - // Filter models based on search - const filteredModels = modelNames.filter((name) => - name.toLowerCase().includes(modelSearch.toLowerCase().trim()) + const modelNames = useMemo( + () => (availableModels ? Object.keys(availableModels.models).sort() : []), + [availableModels] ); + // Filter models based on debounced search + const filteredModels = useMemo( + () => + modelNames.filter((name) => + name.toLowerCase().includes(debouncedSearch.toLowerCase().trim()) + ), + [modelNames, debouncedSearch] + ); + + // Paginated models - only render what's visible + const totalPages = Math.ceil(filteredModels.length / MODELS_PER_PAGE); + const paginatedModels = useMemo(() => { + const start = (currentPage - 1) * MODELS_PER_PAGE; + return filteredModels.slice(start, start + MODELS_PER_PAGE); + }, [filteredModels, currentPage]); + + // Reset to page 1 when search changes + useEffect(() => { + setCurrentPage(1); + }, [debouncedSearch]); + // Fetch available models from the static JSON const fetchAvailableModels = useCallback(async (force = false) => { if (!force && availableModelsCache) { @@ -271,10 +291,6 @@ export function SettingsModal({ (color) => color !== 'dark' && color !== 'gray' && color !== 'white' && color !== 'black' ); - const toggleModelExpand = (modelName: string) => { - setExpandedModel(expandedModel === modelName ? null : modelName); - }; - // Check if a model (base name) is already installed // Returns list of installed tags for the given model name const getInstalledTags = (modelName: string): string[] => { @@ -486,7 +502,9 @@ export function SettingsModal({ {availableModels && ( - {modelNames.length} models + {filteredModels.length === modelNames.length + ? `${modelNames.length} models` + : `${filteredModels.length} of ${modelNames.length} models`} )} @@ -504,48 +522,32 @@ export function SettingsModal({ ) : ( - - {filteredModels.map((modelName) => { - const tags = availableModels?.models[modelName] || []; - const isExpanded = expandedModel === modelName; - const installedTags = getInstalledTags(modelName); + <> + + {paginatedModels.map((modelName) => { + const tags = availableModels?.models[modelName] || []; + const installedTags = getInstalledTags(modelName); - return ( - - {/* Model Header - Clickable */} - toggleModelExpand(modelName)} - > - - - {isExpanded ? ( - - ) : ( - - )} + return ( + + + {modelName} - - - - {tags.length} tags - - {installedTags.length > 0 && ( - - {installedTags.length} installed + + + {tags.length} tags - )} + {installedTags.length > 0 && ( + + {installedTags.length} installed + + )} + - - - - {/* Expanded Tags List */} - - - + + {tags.map((tag) => { const isInstalled = installedTags.includes(tag); @@ -577,18 +579,29 @@ export function SettingsModal({ ); })} - - - - ); - })} + + + ); + })} + - {filteredModels.length === 0 && modelSearch && ( + {filteredModels.length === 0 && debouncedSearch && ( - No models found matching "{modelSearch}" + No models found matching "{debouncedSearch}" )} - + + {totalPages > 1 && ( + + + + )} + )} {availableModels && (