changes
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IconAlertCircle,
|
IconAlertCircle,
|
||||||
IconChevronDown,
|
|
||||||
IconChevronRight,
|
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconPalette,
|
IconPalette,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
@@ -13,19 +11,19 @@ import {
|
|||||||
IconX,
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Collapse,
|
|
||||||
ColorSwatch,
|
ColorSwatch,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
NavLink,
|
NavLink,
|
||||||
|
Pagination,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
rem,
|
rem,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
@@ -36,6 +34,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama';
|
import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama';
|
||||||
|
|
||||||
// Type for the scraped models JSON
|
// Type for the scraped models JSON
|
||||||
@@ -89,24 +88,45 @@ export function SettingsModal({
|
|||||||
const [pullingModel, setPullingModel] = useState<string | null>(null);
|
const [pullingModel, setPullingModel] = useState<string | null>(null);
|
||||||
const [pullError, setPullError] = useState('');
|
const [pullError, setPullError] = useState('');
|
||||||
|
|
||||||
// Expanded model card state
|
// Search state for available models (with debounce for performance)
|
||||||
const [expandedModel, setExpandedModel] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Search state for available models
|
|
||||||
const [modelSearch, setModelSearch] = useState('');
|
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
|
// Track if we've fetched this session
|
||||||
const hasFetchedInstalled = useRef(false);
|
const hasFetchedInstalled = useRef(false);
|
||||||
const hasFetchedAvailable = useRef(false);
|
const hasFetchedAvailable = useRef(false);
|
||||||
|
|
||||||
// Get list of model names sorted alphabetically
|
// Get list of model names sorted alphabetically
|
||||||
const modelNames = availableModels ? Object.keys(availableModels.models).sort() : [];
|
const modelNames = useMemo(
|
||||||
|
() => (availableModels ? Object.keys(availableModels.models).sort() : []),
|
||||||
// Filter models based on search
|
[availableModels]
|
||||||
const filteredModels = modelNames.filter((name) =>
|
|
||||||
name.toLowerCase().includes(modelSearch.toLowerCase().trim())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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
|
// Fetch available models from the static JSON
|
||||||
const fetchAvailableModels = useCallback(async (force = false) => {
|
const fetchAvailableModels = useCallback(async (force = false) => {
|
||||||
if (!force && availableModelsCache) {
|
if (!force && availableModelsCache) {
|
||||||
@@ -271,10 +291,6 @@ export function SettingsModal({
|
|||||||
(color) => color !== 'dark' && color !== 'gray' && color !== 'white' && color !== 'black'
|
(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
|
// Check if a model (base name) is already installed
|
||||||
// Returns list of installed tags for the given model name
|
// Returns list of installed tags for the given model name
|
||||||
const getInstalledTags = (modelName: string): string[] => {
|
const getInstalledTags = (modelName: string): string[] => {
|
||||||
@@ -486,7 +502,9 @@ export function SettingsModal({
|
|||||||
</Text>
|
</Text>
|
||||||
{availableModels && (
|
{availableModels && (
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{modelNames.length} models
|
{filteredModels.length === modelNames.length
|
||||||
|
? `${modelNames.length} models`
|
||||||
|
: `${filteredModels.length} of ${modelNames.length} models`}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
@@ -504,31 +522,19 @@ export function SettingsModal({
|
|||||||
<Loader size="sm" />
|
<Loader size="sm" />
|
||||||
</Group>
|
</Group>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap="xs">
|
<>
|
||||||
{filteredModels.map((modelName) => {
|
<Accordion variant="separated" radius="md">
|
||||||
|
{paginatedModels.map((modelName) => {
|
||||||
const tags = availableModels?.models[modelName] || [];
|
const tags = availableModels?.models[modelName] || [];
|
||||||
const isExpanded = expandedModel === modelName;
|
|
||||||
const installedTags = getInstalledTags(modelName);
|
const installedTags = getInstalledTags(modelName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={modelName} withBorder padding={0} radius="md">
|
<Accordion.Item key={modelName} value={modelName}>
|
||||||
{/* Model Header - Clickable */}
|
<Accordion.Control>
|
||||||
<Box
|
<Group justify="space-between" pr="sm">
|
||||||
p="sm"
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => toggleModelExpand(modelName)}
|
|
||||||
>
|
|
||||||
<Group justify="space-between">
|
|
||||||
<Group gap="sm">
|
|
||||||
{isExpanded ? (
|
|
||||||
<IconChevronDown size={16} />
|
|
||||||
) : (
|
|
||||||
<IconChevronRight size={16} />
|
|
||||||
)}
|
|
||||||
<Text fw={500} size="sm">
|
<Text fw={500} size="sm">
|
||||||
{modelName}
|
{modelName}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Badge size="xs" variant="light" color="gray">
|
<Badge size="xs" variant="light" color="gray">
|
||||||
{tags.length} tags
|
{tags.length} tags
|
||||||
@@ -540,12 +546,8 @@ export function SettingsModal({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
{/* Expanded Tags List */}
|
|
||||||
<Collapse in={isExpanded}>
|
|
||||||
<Divider />
|
|
||||||
<Box p="sm" bg="var(--mantine-color-gray-light)">
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const isInstalled = installedTags.includes(tag);
|
const isInstalled = installedTags.includes(tag);
|
||||||
@@ -577,18 +579,29 @@ export function SettingsModal({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Accordion.Panel>
|
||||||
</Collapse>
|
</Accordion.Item>
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
{filteredModels.length === 0 && modelSearch && (
|
{filteredModels.length === 0 && debouncedSearch && (
|
||||||
<Text c="dimmed" size="sm" ta="center" py="md">
|
<Text c="dimmed" size="sm" ta="center" py="md">
|
||||||
No models found matching "{modelSearch}"
|
No models found matching "{debouncedSearch}"
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Group justify="center" mt="md">
|
||||||
|
<Pagination
|
||||||
|
total={totalPages}
|
||||||
|
value={currentPage}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{availableModels && (
|
{availableModels && (
|
||||||
|
|||||||
Reference in New Issue
Block a user