changes
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IconAlertCircle,
|
IconAlertCircle,
|
||||||
|
IconBrain,
|
||||||
|
IconCode,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
|
IconEye,
|
||||||
IconPalette,
|
IconPalette,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconRobot,
|
IconRobot,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
IconTool,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconX,
|
IconX,
|
||||||
@@ -17,6 +21,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Chip,
|
||||||
ColorSwatch,
|
ColorSwatch,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
@@ -97,6 +102,9 @@ export function SettingsModal({
|
|||||||
const [modelSearch, setModelSearch] = useState('');
|
const [modelSearch, setModelSearch] = useState('');
|
||||||
const [debouncedSearch] = useDebouncedValue(modelSearch, 200);
|
const [debouncedSearch] = useDebouncedValue(modelSearch, 200);
|
||||||
|
|
||||||
|
// Capability filter state
|
||||||
|
const [selectedCapabilities, setSelectedCapabilities] = useState<string[]>([]);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const MODELS_PER_PAGE = 20;
|
const MODELS_PER_PAGE = 20;
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@@ -111,13 +119,25 @@ export function SettingsModal({
|
|||||||
[availableModels]
|
[availableModels]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter models based on debounced search
|
// Filter models based on debounced search and selected capabilities
|
||||||
const filteredModels = useMemo(
|
const filteredModels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
modelNames.filter((name) =>
|
modelNames.filter((name) => {
|
||||||
name.toLowerCase().includes(debouncedSearch.toLowerCase().trim())
|
// Text search filter
|
||||||
),
|
const matchesSearch = name.toLowerCase().includes(debouncedSearch.toLowerCase().trim());
|
||||||
[modelNames, debouncedSearch]
|
if (!matchesSearch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capability filter - if any capabilities selected, model must have ALL of them
|
||||||
|
if (selectedCapabilities.length > 0) {
|
||||||
|
const modelCaps = availableModels?.models[name]?.capabilities || [];
|
||||||
|
return selectedCapabilities.every((cap) => modelCaps.includes(cap));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
[modelNames, debouncedSearch, selectedCapabilities, availableModels]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Paginated models - only render what's visible
|
// Paginated models - only render what's visible
|
||||||
@@ -127,10 +147,10 @@ export function SettingsModal({
|
|||||||
return filteredModels.slice(start, start + MODELS_PER_PAGE);
|
return filteredModels.slice(start, start + MODELS_PER_PAGE);
|
||||||
}, [filteredModels, currentPage]);
|
}, [filteredModels, currentPage]);
|
||||||
|
|
||||||
// Reset to page 1 when search changes
|
// Reset to page 1 when search or filter changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [debouncedSearch]);
|
}, [debouncedSearch, selectedCapabilities]);
|
||||||
|
|
||||||
// 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) => {
|
||||||
@@ -312,6 +332,12 @@ export function SettingsModal({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get capabilities for an installed model by extracting base name
|
||||||
|
const getModelCapabilities = (fullModelName: string): string[] => {
|
||||||
|
const [baseName] = fullModelName.split(':');
|
||||||
|
return availableModels?.models[baseName]?.capabilities || [];
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
@@ -464,7 +490,9 @@ export function SettingsModal({
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{installedModels.map((model) => (
|
{installedModels.map((model) => {
|
||||||
|
const capabilities = getModelCapabilities(model.name);
|
||||||
|
return (
|
||||||
<Card key={model.digest} withBorder padding="sm" radius="md">
|
<Card key={model.digest} withBorder padding="sm" radius="md">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -482,6 +510,30 @@ export function SettingsModal({
|
|||||||
{model.details.quantization_level}
|
{model.details.quantization_level}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
|
{capabilities.length > 0 && (
|
||||||
|
<Group gap="xs" mt={4}>
|
||||||
|
{capabilities.map((cap) => (
|
||||||
|
<Badge
|
||||||
|
key={cap}
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color={
|
||||||
|
cap === 'vision'
|
||||||
|
? 'violet'
|
||||||
|
: cap === 'tools'
|
||||||
|
? 'blue'
|
||||||
|
: cap === 'thinking'
|
||||||
|
? 'orange'
|
||||||
|
: cap === 'embedding'
|
||||||
|
? 'teal'
|
||||||
|
: 'gray'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cap}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
color="red"
|
color="red"
|
||||||
@@ -492,7 +544,8 @@ export function SettingsModal({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -519,9 +572,128 @@ export function SettingsModal({
|
|||||||
leftSection={<IconSearch size={16} />}
|
leftSection={<IconSearch size={16} />}
|
||||||
value={modelSearch}
|
value={modelSearch}
|
||||||
onChange={(e) => setModelSearch(e.currentTarget.value)}
|
onChange={(e) => setModelSearch(e.currentTarget.value)}
|
||||||
mb="sm"
|
mb="xs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Capability Filter */}
|
||||||
|
<Chip.Group
|
||||||
|
multiple
|
||||||
|
value={selectedCapabilities}
|
||||||
|
onChange={setSelectedCapabilities}
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
gap={0}
|
||||||
|
mb="sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--mantine-color-default-hover)',
|
||||||
|
borderRadius: 'var(--mantine-radius-xl)',
|
||||||
|
padding: rem(4),
|
||||||
|
border: '1px solid var(--mantine-color-default-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
value="vision"
|
||||||
|
variant="filled"
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
paddingLeft: rem(10),
|
||||||
|
paddingRight: rem(10),
|
||||||
|
background: selectedCapabilities.includes('vision')
|
||||||
|
? 'linear-gradient(135deg, var(--mantine-color-violet-5), var(--mantine-color-grape-5))'
|
||||||
|
: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: selectedCapabilities.includes('vision')
|
||||||
|
? 'white'
|
||||||
|
: 'var(--mantine-color-dimmed)',
|
||||||
|
},
|
||||||
|
iconWrapper: { display: 'none' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<IconEye size={12} />
|
||||||
|
Vision
|
||||||
|
</Group>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
value="tools"
|
||||||
|
variant="filled"
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
paddingLeft: rem(10),
|
||||||
|
paddingRight: rem(10),
|
||||||
|
background: selectedCapabilities.includes('tools')
|
||||||
|
? 'linear-gradient(135deg, var(--mantine-color-blue-5), var(--mantine-color-cyan-5))'
|
||||||
|
: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: selectedCapabilities.includes('tools')
|
||||||
|
? 'white'
|
||||||
|
: 'var(--mantine-color-dimmed)',
|
||||||
|
},
|
||||||
|
iconWrapper: { display: 'none' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<IconTool size={12} />
|
||||||
|
Tools
|
||||||
|
</Group>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
value="thinking"
|
||||||
|
variant="filled"
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
paddingLeft: rem(10),
|
||||||
|
paddingRight: rem(10),
|
||||||
|
background: selectedCapabilities.includes('thinking')
|
||||||
|
? 'linear-gradient(135deg, var(--mantine-color-orange-5), var(--mantine-color-yellow-5))'
|
||||||
|
: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: selectedCapabilities.includes('thinking')
|
||||||
|
? 'white'
|
||||||
|
: 'var(--mantine-color-dimmed)',
|
||||||
|
},
|
||||||
|
iconWrapper: { display: 'none' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<IconBrain size={12} />
|
||||||
|
Thinking
|
||||||
|
</Group>
|
||||||
|
</Chip>
|
||||||
|
<Chip
|
||||||
|
value="embedding"
|
||||||
|
variant="filled"
|
||||||
|
radius="xl"
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
paddingLeft: rem(10),
|
||||||
|
paddingRight: rem(10),
|
||||||
|
background: selectedCapabilities.includes('embedding')
|
||||||
|
? 'linear-gradient(135deg, var(--mantine-color-teal-5), var(--mantine-color-green-5))'
|
||||||
|
: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: selectedCapabilities.includes('embedding')
|
||||||
|
? 'white'
|
||||||
|
: 'var(--mantine-color-dimmed)',
|
||||||
|
},
|
||||||
|
iconWrapper: { display: 'none' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<IconCode size={12} />
|
||||||
|
Embedding
|
||||||
|
</Group>
|
||||||
|
</Chip>
|
||||||
|
</Group>
|
||||||
|
</Chip.Group>
|
||||||
|
|
||||||
{loadingAvailable ? (
|
{loadingAvailable ? (
|
||||||
<Group justify="center" py="md">
|
<Group justify="center" py="md">
|
||||||
<Loader size="sm" />
|
<Loader size="sm" />
|
||||||
|
|||||||
Reference in New Issue
Block a user