mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
new integrated hugginface model search, ensure that prettier --check is run in CI
This commit is contained in:
parent
d302f07e79
commit
bde2decd9e
19 changed files with 2345 additions and 213 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Run checks (lint, type check, spell check)
|
- name: Run checks (format, lint, type check)
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
- name: Test build
|
- name: Test build
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src 'self' http: https:; script-src 'self' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' data:; frame-src 'self' http: https:;"
|
content="default-src 'self'; connect-src 'self' http: https:; script-src 'self' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' data:; frame-src 'self' http: https:; img-src 'self' data: https:;"
|
||||||
/>
|
/>
|
||||||
<title>Gerbil</title>
|
<title>Gerbil</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@
|
||||||
"package": "electron-vite build && electron-builder --publish=never",
|
"package": "electron-vite build && electron-builder --publish=never",
|
||||||
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
|
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
|
||||||
"format": "prettier --write . --ignore-path .gitignore | grep -v '(unchanged)' || true",
|
"format": "prettier --write . --ignore-path .gitignore | grep -v '(unchanged)' || true",
|
||||||
|
"format:check": "prettier --check . --ignore-path .gitignore",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"compile": "tsc --noEmit",
|
"compile": "tsc --noEmit",
|
||||||
"test": "yarn lint && yarn compile",
|
"test": "yarn format:check && yarn lint && yarn compile",
|
||||||
"release": "yarn dlx tsx scripts/release.ts"
|
"release": "yarn dlx tsx scripts/release.ts"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
@ -53,6 +54,10 @@
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"systeminformation": "^5.27.11",
|
"systeminformation": "^5.27.11",
|
||||||
"winston": "^3.19.0",
|
"winston": "^3.19.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
|
|
@ -85,7 +90,7 @@
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"rollup-plugin-visualizer": "^6.0.5",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.6"
|
"vite": "^7.2.7"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.gerbil.app",
|
"appId": "com.gerbil.app",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export interface ModalProps {
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean;
|
||||||
closeOnEscape?: boolean;
|
closeOnEscape?: boolean;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
|
tallContent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = ({
|
export const Modal = ({
|
||||||
|
|
@ -32,20 +33,24 @@ export const Modal = ({
|
||||||
closeOnClickOutside = false,
|
closeOnClickOutside = false,
|
||||||
closeOnEscape = true,
|
closeOnEscape = true,
|
||||||
showCloseButton = false,
|
showCloseButton = false,
|
||||||
|
tallContent = false,
|
||||||
...props
|
...props
|
||||||
}: ModalProps) => (
|
}: ModalProps) => {
|
||||||
<MantineModal
|
const content = tallContent ? (
|
||||||
opened={opened}
|
<div
|
||||||
onClose={onClose}
|
style={{
|
||||||
title={title}
|
height: '66vh',
|
||||||
size={size}
|
padding: 0,
|
||||||
centered
|
display: 'flex',
|
||||||
closeOnClickOutside={closeOnClickOutside}
|
flexDirection: 'column',
|
||||||
closeOnEscape={closeOnEscape}
|
position: 'relative',
|
||||||
styles={MODAL_STYLES_WITH_TITLEBAR}
|
}}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -60,5 +65,22 @@ export const Modal = ({
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineModal
|
||||||
|
opened={opened}
|
||||||
|
onClose={onClose}
|
||||||
|
title={title}
|
||||||
|
size={size}
|
||||||
|
centered
|
||||||
|
closeOnClickOutside={closeOnClickOutside}
|
||||||
|
closeOnEscape={closeOnEscape}
|
||||||
|
styles={MODAL_STYLES_WITH_TITLEBAR}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
</MantineModal>
|
</MantineModal>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,11 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
tooltip="Select a GGUF text generation model file for chat and completion tasks."
|
tooltip="Select a GGUF text generation model file for chat and completion tasks."
|
||||||
onChange={setModel}
|
onChange={setModel}
|
||||||
onSelectFile={() => selectFile('model', 'Select Text Model')}
|
onSelectFile={() => selectFile('model', 'Select Text Model')}
|
||||||
searchUrl="https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending"
|
searchParams={{
|
||||||
|
pipelineTag: 'text-generation',
|
||||||
|
filter: 'gguf',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
showAnalyze
|
showAnalyze
|
||||||
paramType="model"
|
paramType="model"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Table, Text, Group, Loader } from '@mantine/core';
|
||||||
|
import { formatBytes } from '@/utils/format';
|
||||||
|
import type { HuggingFaceFileInfo } from '@/types';
|
||||||
|
|
||||||
|
interface FilesTableProps {
|
||||||
|
files: HuggingFaceFileInfo[];
|
||||||
|
loading: boolean;
|
||||||
|
onSelect: (file: HuggingFaceFileInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilesTable = ({ files, loading, onSelect }: FilesTableProps) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Group justify="center" py="xl">
|
||||||
|
<Loader />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">
|
||||||
|
No compatible files found in this model
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>File</Table.Th>
|
||||||
|
<Table.Th style={{ width: 120 }}>Size</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{files.map((file) => (
|
||||||
|
<Table.Tr
|
||||||
|
key={file.path}
|
||||||
|
onClick={() => onSelect(file)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" lineClamp={1}>
|
||||||
|
{file.path}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">{formatBytes(file.size)}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Accordion, Paper, Group, Loader, Text } from '@mantine/core';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
|
interface ModelCardProps {
|
||||||
|
modelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
|
const [readme, setReadme] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadReadme = async () => {
|
||||||
|
if (readme !== null) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${HUGGINGFACE_BASE_URL}/${modelId}/raw/main/README.md`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('README not available');
|
||||||
|
}
|
||||||
|
let text = await response.text();
|
||||||
|
|
||||||
|
const frontmatterRegex = /^---\n[\s\S]*?\n---\n/;
|
||||||
|
text = text.replace(frontmatterRegex, '');
|
||||||
|
|
||||||
|
setReadme(text);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load README');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<Accordion.Item value="readme">
|
||||||
|
<Accordion.Control onClick={loadReadme}>Model Card</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
{loading && (
|
||||||
|
<Group justify="center" py="md">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{readme && (
|
||||||
|
<Paper
|
||||||
|
p="md"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
overflow: 'auto',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||||
|
components={{
|
||||||
|
img: ({ node, ...props }) => (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
pre: ({ node, ...props }) => (
|
||||||
|
<pre
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
overflow: 'auto',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
code: ({ node, ...props }) => (
|
||||||
|
<code
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{readme}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Stack,
|
||||||
|
Badge,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Download, Heart, Lock } from 'lucide-react';
|
||||||
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
import { formatDownloads, formatDate } from '@/utils/format';
|
||||||
|
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
|
||||||
|
|
||||||
|
interface ModelsTableProps {
|
||||||
|
models: HuggingFaceModelInfo[];
|
||||||
|
loading: boolean;
|
||||||
|
onSelect: (model: HuggingFaceModelInfo) => void;
|
||||||
|
sortBy: HuggingFaceSortOption;
|
||||||
|
onSortChange: (sort: HuggingFaceSortOption) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelsTable = ({
|
||||||
|
models,
|
||||||
|
loading,
|
||||||
|
onSelect,
|
||||||
|
sortBy,
|
||||||
|
onSortChange,
|
||||||
|
}: ModelsTableProps) => {
|
||||||
|
if (loading && models.length === 0) {
|
||||||
|
return (
|
||||||
|
<Group justify="center" py="xl">
|
||||||
|
<Loader />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
return (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">
|
||||||
|
No models found
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Model</Table.Th>
|
||||||
|
<Table.Th style={{ width: 80 }}>Params</Table.Th>
|
||||||
|
<Table.Th
|
||||||
|
style={{ width: 120, cursor: 'pointer' }}
|
||||||
|
onClick={() => onSortChange('downloads')}
|
||||||
|
>
|
||||||
|
Downloads{sortBy === 'downloads' && ' ↓'}
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th
|
||||||
|
style={{ width: 90, cursor: 'pointer' }}
|
||||||
|
onClick={() => onSortChange('likes')}
|
||||||
|
>
|
||||||
|
Likes{sortBy === 'likes' && ' ↓'}
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{models.map((model) => (
|
||||||
|
<Table.Tr
|
||||||
|
key={model.id}
|
||||||
|
onClick={() => {
|
||||||
|
if (model.gated) {
|
||||||
|
window.electronAPI.app.openExternal(
|
||||||
|
`${HUGGINGFACE_BASE_URL}/${model.id}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onSelect(model);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<Text size="sm" fw={500} lineClamp={1}>
|
||||||
|
{model.name}
|
||||||
|
</Text>
|
||||||
|
{model.gated && (
|
||||||
|
<Tooltip label="Gated model - click to open on HuggingFace">
|
||||||
|
<Badge
|
||||||
|
size="xs"
|
||||||
|
color="yellow"
|
||||||
|
leftSection={<Lock size={10} />}
|
||||||
|
>
|
||||||
|
Gated
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{model.author}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Updated {formatDate(model.updatedAt)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{model.paramSize && (
|
||||||
|
<Badge size="sm" variant="light" color="blue">
|
||||||
|
{model.paramSize}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap={4}>
|
||||||
|
<Download size={12} />
|
||||||
|
<Text size="sm">{formatDownloads(model.downloads)}</Text>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap={4}>
|
||||||
|
<Heart size={12} />
|
||||||
|
<Text size="sm">{formatDownloads(model.likes)}</Text>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
235
src/components/screens/Launch/HuggingFaceSearchModal/index.tsx
Normal file
235
src/components/screens/Launch/HuggingFaceSearchModal/index.tsx
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
Loader,
|
||||||
|
Alert,
|
||||||
|
ScrollArea,
|
||||||
|
SegmentedControl,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { Search, ArrowLeft, ExternalLink } from 'lucide-react';
|
||||||
|
import { Modal } from '@/components/Modal';
|
||||||
|
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
|
||||||
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
import type {
|
||||||
|
HuggingFaceModelInfo,
|
||||||
|
HuggingFaceFileInfo,
|
||||||
|
HuggingFaceSortOption,
|
||||||
|
HuggingFaceSearchParams,
|
||||||
|
} from '@/types';
|
||||||
|
import { ModelsTable } from './ModelsTable';
|
||||||
|
import { FilesTable } from './FilesTable';
|
||||||
|
import { ModelCard } from './ModelCard';
|
||||||
|
|
||||||
|
interface HuggingFaceSearchModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSelect: (url: string) => void;
|
||||||
|
searchParams: HuggingFaceSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HuggingFaceSearchModal = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
onSelect,
|
||||||
|
searchParams: initialSearchParams,
|
||||||
|
}: HuggingFaceSearchModalProps) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState(
|
||||||
|
initialSearchParams.search || ''
|
||||||
|
);
|
||||||
|
const [debouncedQuery] = useDebouncedValue(searchQuery, 300);
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const prevOpenedRef = useRef(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
models,
|
||||||
|
files,
|
||||||
|
loading,
|
||||||
|
loadingFiles,
|
||||||
|
error,
|
||||||
|
hasMore,
|
||||||
|
selectedModel,
|
||||||
|
sortBy,
|
||||||
|
searchParams,
|
||||||
|
searchModels,
|
||||||
|
loadMoreModels,
|
||||||
|
loadModelFiles,
|
||||||
|
changeSortOrder,
|
||||||
|
getFileDownloadUrl,
|
||||||
|
reset,
|
||||||
|
} = useHuggingFaceSearch(initialSearchParams);
|
||||||
|
|
||||||
|
const handleSortChange = (value: string) => {
|
||||||
|
changeSortOrder(value as HuggingFaceSortOption, searchQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened && !prevOpenedRef.current) {
|
||||||
|
searchModels();
|
||||||
|
}
|
||||||
|
if (!opened && prevOpenedRef.current) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
prevOpenedRef.current = opened;
|
||||||
|
}, [opened, searchModels, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened && debouncedQuery !== undefined) {
|
||||||
|
searchModels(debouncedQuery);
|
||||||
|
}
|
||||||
|
}, [debouncedQuery, opened, searchModels]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSearchQuery(initialSearchParams.search || '');
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModelSelect = (model: HuggingFaceModelInfo) => {
|
||||||
|
loadModelFiles(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (file: HuggingFaceFileInfo) => {
|
||||||
|
if (!selectedModel) return;
|
||||||
|
const url = getFileDownloadUrl(selectedModel.id, file.path);
|
||||||
|
onSelect(url);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
reset();
|
||||||
|
searchModels(searchQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenExternal = () => {
|
||||||
|
if (selectedModel) {
|
||||||
|
window.electronAPI.app.openExternal(
|
||||||
|
`${HUGGINGFACE_BASE_URL}/${selectedModel.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterBadges = () => {
|
||||||
|
const badges: string[] = [];
|
||||||
|
if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase());
|
||||||
|
if (searchParams?.pipelineTag)
|
||||||
|
badges.push(searchParams.pipelineTag.replace('-', ' '));
|
||||||
|
return badges;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={handleClose}
|
||||||
|
title="Search Hugging Face Models"
|
||||||
|
size="xl"
|
||||||
|
tallContent
|
||||||
|
>
|
||||||
|
<Stack gap="md" style={{ height: '100%' }}>
|
||||||
|
{selectedModel ? (
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<ActionIcon variant="subtle" onClick={handleBack}>
|
||||||
|
<ArrowLeft size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Text size="sm" fw={500} lineClamp={1}>
|
||||||
|
{selectedModel.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{selectedModel.author}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Tooltip label="Open on Hugging Face">
|
||||||
|
<ActionIcon variant="subtle" onClick={handleOpenExternal}>
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
) : (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search models..."
|
||||||
|
leftSection={<Search size={16} />}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||||
|
rightSection={loading ? <Loader size={16} /> : null}
|
||||||
|
/>
|
||||||
|
<Group gap="xs" align="center" justify="space-between">
|
||||||
|
<SegmentedControl
|
||||||
|
size="xs"
|
||||||
|
value={sortBy}
|
||||||
|
onChange={handleSortChange}
|
||||||
|
data={[
|
||||||
|
{ label: 'Trending', value: 'trendingScore' },
|
||||||
|
{ label: 'Downloads', value: 'downloads' },
|
||||||
|
{ label: 'Likes', value: 'likes' },
|
||||||
|
{ label: 'Updated', value: 'lastModified' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Group gap="xs">
|
||||||
|
{getFilterBadges().map((badge) => (
|
||||||
|
<Badge key={badge} size="sm" variant="light">
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert color="red" title="Error">
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollArea
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onScrollPositionChange={({ y }) => {
|
||||||
|
const target = scrollAreaRef.current;
|
||||||
|
if (!target) return;
|
||||||
|
const isNearBottom =
|
||||||
|
target.scrollHeight - y <= target.clientHeight + 100;
|
||||||
|
if (isNearBottom && hasMore && !loading && !selectedModel) {
|
||||||
|
loadMoreModels();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
viewportRef={scrollAreaRef}
|
||||||
|
>
|
||||||
|
{selectedModel ? (
|
||||||
|
<Stack gap="md">
|
||||||
|
<ModelCard modelId={selectedModel.id} />
|
||||||
|
<FilesTable
|
||||||
|
files={files}
|
||||||
|
loading={loadingFiles}
|
||||||
|
onSelect={handleFileSelect}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<ModelsTable
|
||||||
|
models={models}
|
||||||
|
loading={loading}
|
||||||
|
onSelect={handleModelSelect}
|
||||||
|
sortBy={sortBy}
|
||||||
|
onSortChange={(sort) => changeSortOrder(sort, searchQuery)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{loading && !selectedModel && models.length > 0 && (
|
||||||
|
<Group justify="center">
|
||||||
|
<Loader size="sm" />
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Loading more...
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -69,7 +69,11 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="The primary image generation model. This is the main model that will generate images."
|
tooltip="The primary image generation model. This is the main model that will generate images."
|
||||||
onChange={setSdmodel}
|
onChange={setSdmodel}
|
||||||
onSelectFile={() => selectFile('sdmodel', 'Select Image Model')}
|
onSelectFile={() => selectFile('sdmodel', 'Select Image Model')}
|
||||||
searchUrl="https://huggingface.co/models?pipeline_tag=text-to-image&library=gguf&sort=trending"
|
searchParams={{
|
||||||
|
pipelineTag: 'text-to-image',
|
||||||
|
filter: 'gguf',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
showAnalyze
|
showAnalyze
|
||||||
paramType="sdmodel"
|
paramType="sdmodel"
|
||||||
/>
|
/>
|
||||||
|
|
@ -81,7 +85,11 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="T5-XXL text encoder model for advanced text understanding."
|
tooltip="T5-XXL text encoder model for advanced text understanding."
|
||||||
onChange={setSdt5xxl}
|
onChange={setSdt5xxl}
|
||||||
onSelectFile={() => selectFile('sdt5xxl', 'Select T5XXL Model')}
|
onSelectFile={() => selectFile('sdt5xxl', 'Select T5XXL Model')}
|
||||||
searchUrl="https://huggingface.co/models?search=t5-xxl&library=gguf&sort=trending"
|
searchParams={{
|
||||||
|
search: 't5xxl',
|
||||||
|
filter: 'safetensors',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
paramType="sdt5xxl"
|
paramType="sdt5xxl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -92,7 +100,11 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="CLIP-L text encoder model for text-image understanding."
|
tooltip="CLIP-L text encoder model for text-image understanding."
|
||||||
onChange={setSdclipl}
|
onChange={setSdclipl}
|
||||||
onSelectFile={() => selectFile('sdclipl', 'Select CLIP-L Model')}
|
onSelectFile={() => selectFile('sdclipl', 'Select CLIP-L Model')}
|
||||||
searchUrl="https://huggingface.co/models?search=clip-l&library=gguf&sort=trending"
|
searchParams={{
|
||||||
|
search: 'clip',
|
||||||
|
filter: 'safetensors',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
paramType="sdclipl"
|
paramType="sdclipl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -100,10 +112,14 @@ export const ImageGenerationTab = () => {
|
||||||
label="Clip-G File"
|
label="Clip-G File"
|
||||||
value={sdclipg}
|
value={sdclipg}
|
||||||
placeholder="Select a Clip-G file or enter a direct URL"
|
placeholder="Select a Clip-G file or enter a direct URL"
|
||||||
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
|
tooltip="CLIP-G text encoder model for enhanced text-image understanding, or mmproj files for vision-language models."
|
||||||
onChange={setSdclipg}
|
onChange={setSdclipg}
|
||||||
onSelectFile={() => selectFile('sdclipg', 'Select CLIP-G Model')}
|
onSelectFile={() => selectFile('sdclipg', 'Select CLIP-G Model')}
|
||||||
searchUrl="https://huggingface.co/models?search=clip-g&library=gguf&sort=trending"
|
searchParams={{
|
||||||
|
search: 'clip',
|
||||||
|
filter: 'gguf',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
paramType="sdclipg"
|
paramType="sdclipg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -116,7 +132,11 @@ export const ImageGenerationTab = () => {
|
||||||
onSelectFile={() =>
|
onSelectFile={() =>
|
||||||
selectFile('sdphotomaker', 'Select PhotoMaker Model')
|
selectFile('sdphotomaker', 'Select PhotoMaker Model')
|
||||||
}
|
}
|
||||||
searchUrl="https://huggingface.co/models?search=photomaker&library=safetensors&sort=trending"
|
searchParams={{
|
||||||
|
search: 'photomaker',
|
||||||
|
filter: 'safetensors',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
paramType="sdphotomaker"
|
paramType="sdphotomaker"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -127,7 +147,11 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="Variational Autoencoder model for improved image quality."
|
tooltip="Variational Autoencoder model for improved image quality."
|
||||||
onChange={setSdvae}
|
onChange={setSdvae}
|
||||||
onSelectFile={() => selectFile('sdvae', 'Select VAE Model')}
|
onSelectFile={() => selectFile('sdvae', 'Select VAE Model')}
|
||||||
searchUrl="https://huggingface.co/models?search=vae&library=safetensors&sort=trending"
|
searchParams={{
|
||||||
|
search: 'vae',
|
||||||
|
filter: 'safetensors',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
paramType="sdvae"
|
paramType="sdvae"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -138,7 +162,11 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
|
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
|
||||||
onChange={setSdlora}
|
onChange={setSdlora}
|
||||||
onSelectFile={() => selectFile('sdlora', 'Select LoRA Model')}
|
onSelectFile={() => selectFile('sdlora', 'Select LoRA Model')}
|
||||||
searchUrl="https://huggingface.co/models?search=lora&library=safetensors&sort=trending"
|
searchParams={{
|
||||||
|
search: 'lora',
|
||||||
|
filter: 'safetensors',
|
||||||
|
sort: 'trendingScore',
|
||||||
|
}}
|
||||||
paramType="sdlora"
|
paramType="sdlora"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,15 @@ import {
|
||||||
import { File, Search, Info } from 'lucide-react';
|
import { File, Search, Info } from 'lucide-react';
|
||||||
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
||||||
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
|
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
|
||||||
|
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
|
||||||
import { getInputValidationState } from '@/utils/validation';
|
import { getInputValidationState } from '@/utils/validation';
|
||||||
import { logError } from '@/utils/logger';
|
import { logError } from '@/utils/logger';
|
||||||
import type { ModelAnalysis, ModelParamType, CachedModel } from '@/types';
|
import type {
|
||||||
|
ModelAnalysis,
|
||||||
|
ModelParamType,
|
||||||
|
CachedModel,
|
||||||
|
HuggingFaceSearchParams,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
interface ModelFileFieldProps {
|
interface ModelFileFieldProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -22,7 +28,7 @@ interface ModelFileFieldProps {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onSelectFile: () => void;
|
onSelectFile: () => void;
|
||||||
searchUrl?: string;
|
searchParams?: HuggingFaceSearchParams;
|
||||||
showAnalyze?: boolean;
|
showAnalyze?: boolean;
|
||||||
paramType: ModelParamType;
|
paramType: ModelParamType;
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +40,7 @@ export const ModelFileField = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
onChange,
|
onChange,
|
||||||
onSelectFile,
|
onSelectFile,
|
||||||
searchUrl,
|
searchParams,
|
||||||
showAnalyze = false,
|
showAnalyze = false,
|
||||||
paramType,
|
paramType,
|
||||||
}: ModelFileFieldProps) => {
|
}: ModelFileFieldProps) => {
|
||||||
|
|
@ -46,6 +52,7 @@ export const ModelFileField = ({
|
||||||
const [analysisLoading, setAnalysisLoading] = useState(false);
|
const [analysisLoading, setAnalysisLoading] = useState(false);
|
||||||
const [analysisError, setAnalysisError] = useState<string>();
|
const [analysisError, setAnalysisError] = useState<string>();
|
||||||
const [cachedModels, setCachedModels] = useState<CachedModel[]>([]);
|
const [cachedModels, setCachedModels] = useState<CachedModel[]>([]);
|
||||||
|
const [searchModalOpened, setSearchModalOpened] = useState(false);
|
||||||
const combobox = useCombobox();
|
const combobox = useCombobox();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -141,10 +148,10 @@ export const ModelFileField = ({
|
||||||
>
|
>
|
||||||
Browse
|
Browse
|
||||||
</Button>
|
</Button>
|
||||||
{searchUrl && (
|
{searchParams && (
|
||||||
<Tooltip label="Search Hugging Face">
|
<Tooltip label="Search Hugging Face">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => window.electronAPI.app.openExternal(searchUrl)}
|
onClick={() => setSearchModalOpened(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
|
|
@ -182,6 +189,15 @@ export const ModelFileField = ({
|
||||||
loading={analysisLoading}
|
loading={analysisLoading}
|
||||||
error={analysisError}
|
error={analysisError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{searchParams && (
|
||||||
|
<HuggingFaceSearchModal
|
||||||
|
opened={searchModalOpened}
|
||||||
|
onClose={() => setSearchModalOpened(false)}
|
||||||
|
onSelect={onChange}
|
||||||
|
searchParams={searchParams}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -67,15 +67,7 @@ export const SettingsModal = ({
|
||||||
}
|
}
|
||||||
size="xl"
|
size="xl"
|
||||||
showCloseButton
|
showCloseButton
|
||||||
>
|
tallContent
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '66vh',
|
|
||||||
padding: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={effectiveActiveTab}
|
value={effectiveActiveTab}
|
||||||
|
|
@ -103,9 +95,7 @@ export const SettingsModal = ({
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="general"
|
value="general"
|
||||||
leftSection={
|
leftSection={
|
||||||
<SlidersHorizontal
|
<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
General
|
General
|
||||||
|
|
@ -138,9 +128,7 @@ export const SettingsModal = ({
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="troubleshooting"
|
value="troubleshooting"
|
||||||
leftSection={
|
leftSection={<Wrench style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<Wrench style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Troubleshooting
|
Troubleshooting
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
@ -178,7 +166,6 @@ export const SettingsModal = ({
|
||||||
<AboutTab />
|
<AboutTab />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
export interface ImageModelPreset {
|
export interface ImageModelPreset {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly sdmodel: string;
|
readonly sdmodel: string;
|
||||||
|
|
@ -11,53 +13,38 @@ export interface ImageModelPreset {
|
||||||
export const IMAGE_MODEL_PRESETS: readonly ImageModelPreset[] = [
|
export const IMAGE_MODEL_PRESETS: readonly ImageModelPreset[] = [
|
||||||
{
|
{
|
||||||
name: 'Z-Image',
|
name: 'Z-Image',
|
||||||
sdmodel:
|
sdmodel: `${HUGGINGFACE_BASE_URL}/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_0.gguf`,
|
||||||
'https://huggingface.co/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_0.gguf',
|
|
||||||
sdt5xxl: '',
|
sdt5xxl: '',
|
||||||
sdclipl:
|
sdclipl: `${HUGGINGFACE_BASE_URL}/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_S.gguf`,
|
||||||
'https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_S.gguf',
|
|
||||||
sdclipg: '',
|
sdclipg: '',
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
sdvae:
|
sdvae: `${HUGGINGFACE_BASE_URL}/koboldcpp/GGUFDumps/resolve/main/flux1vae.safetensors`,
|
||||||
'https://huggingface.co/koboldcpp/GGUFDumps/resolve/main/flux1vae.safetensors',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'FLUX.1',
|
name: 'FLUX.1',
|
||||||
sdmodel:
|
sdmodel: `${HUGGINGFACE_BASE_URL}/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf`,
|
||||||
'https://huggingface.co/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf',
|
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
|
||||||
sdt5xxl:
|
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
|
||||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors',
|
|
||||||
sdclipl:
|
|
||||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors',
|
|
||||||
sdclipg: '',
|
sdclipg: '',
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
sdvae:
|
sdvae: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/ae.safetensors`,
|
||||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chroma',
|
name: 'Chroma',
|
||||||
sdmodel:
|
sdmodel: `${HUGGINGFACE_BASE_URL}/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v45/chroma-unlocked-v45-Q4_0.gguf`,
|
||||||
'https://huggingface.co/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v45/chroma-unlocked-v45-Q4_0.gguf',
|
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
|
||||||
sdt5xxl:
|
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
|
||||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors',
|
|
||||||
sdclipl:
|
|
||||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors',
|
|
||||||
sdclipg: '',
|
sdclipg: '',
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
sdvae:
|
sdvae: `${HUGGINGFACE_BASE_URL}/lodestones/Chroma/resolve/main/ae.safetensors`,
|
||||||
'https://huggingface.co/lodestones/Chroma/resolve/main/ae.safetensors',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Qwen Image Edit 2509',
|
name: 'Qwen Image Edit 2509',
|
||||||
sdmodel:
|
sdmodel: `${HUGGINGFACE_BASE_URL}/QuantStack/Qwen-Image-Edit-2509-GGUF/resolve/main/Qwen-Image-Edit-2509-Q4_K_S.gguf`,
|
||||||
'https://huggingface.co/QuantStack/Qwen-Image-Edit-2509-GGUF/resolve/main/Qwen-Image-Edit-2509-Q4_K_S.gguf',
|
|
||||||
sdt5xxl: '',
|
sdt5xxl: '',
|
||||||
sdclipl:
|
sdclipl: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf`,
|
||||||
'https://huggingface.co/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf',
|
sdclipg: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf`,
|
||||||
sdclipg:
|
|
||||||
'https://huggingface.co/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf',
|
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
sdvae:
|
sdvae: `${HUGGINGFACE_BASE_URL}/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors`,
|
||||||
'https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors',
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ export const TITLEBAR_HEIGHT = '2.5rem';
|
||||||
|
|
||||||
export const STATUSBAR_HEIGHT = '1.5rem';
|
export const STATUSBAR_HEIGHT = '1.5rem';
|
||||||
|
|
||||||
|
export const HUGGINGFACE_BASE_URL = 'https://huggingface.co';
|
||||||
|
|
||||||
export const SERVER_READY_SIGNALS = {
|
export const SERVER_READY_SIGNALS = {
|
||||||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||||
SILLYTAVERN: 'SillyTavern is listening on',
|
SILLYTAVERN: 'SillyTavern is listening on',
|
||||||
|
|
@ -14,8 +16,7 @@ export const SERVER_READY_SIGNALS = {
|
||||||
|
|
||||||
export const DEFAULT_CONTEXT_SIZE = 4096;
|
export const DEFAULT_CONTEXT_SIZE = 4096;
|
||||||
|
|
||||||
export const DEFAULT_MODEL_URL =
|
export const DEFAULT_MODEL_URL = `${HUGGINGFACE_BASE_URL}/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf`;
|
||||||
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf';
|
|
||||||
|
|
||||||
export const DEFAULT_AUTO_GPU_LAYERS = true;
|
export const DEFAULT_AUTO_GPU_LAYERS = true;
|
||||||
|
|
||||||
|
|
|
||||||
356
src/hooks/useHuggingFaceSearch.ts
Normal file
356
src/hooks/useHuggingFaceSearch.ts
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
import { useState, useCallback, useRef } from 'react';
|
||||||
|
import { logError } from '@/utils/logger';
|
||||||
|
import type {
|
||||||
|
HuggingFaceModelInfo,
|
||||||
|
HuggingFaceFileInfo,
|
||||||
|
HuggingFaceSearchParams,
|
||||||
|
HuggingFaceSortOption,
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
|
const MODELS_PER_PAGE = 20;
|
||||||
|
const HF_API_BASE = `${HUGGINGFACE_BASE_URL}/api`;
|
||||||
|
|
||||||
|
interface HFApiModel {
|
||||||
|
id: string;
|
||||||
|
downloads: number;
|
||||||
|
likes: number;
|
||||||
|
lastModified: string;
|
||||||
|
gated: boolean | 'auto' | 'manual';
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HFApiFile {
|
||||||
|
type: 'file' | 'directory';
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lfs?: {
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HFApiBaseModel {
|
||||||
|
safetensors?: {
|
||||||
|
parameters?: {
|
||||||
|
BF16?: number;
|
||||||
|
F32?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidModelId = (id: string) => id.includes('/');
|
||||||
|
|
||||||
|
const extractParamSize = (name: string) => {
|
||||||
|
const match = name.match(/(\d+(?:\.\d+)?[BM])(?!\d)/i);
|
||||||
|
return match ? match[1].toUpperCase() : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractBaseModelId = (tags: string[]) => {
|
||||||
|
const baseModelTag = tags.find(
|
||||||
|
(tag) => tag.startsWith('base_model:') && !tag.includes('quantized')
|
||||||
|
);
|
||||||
|
return baseModelTag?.replace('base_model:', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatParamCount = (paramCount: number) => {
|
||||||
|
if (paramCount >= 1_000_000_000) {
|
||||||
|
const billions = paramCount / 1_000_000_000;
|
||||||
|
return billions % 1 === 0 ? `${billions}B` : `${billions.toFixed(1)}B`;
|
||||||
|
}
|
||||||
|
if (paramCount >= 1_000_000) {
|
||||||
|
const millions = paramCount / 1_000_000;
|
||||||
|
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
return `${paramCount}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBaseModelParams = async (baseModelId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${HF_API_BASE}/models/${baseModelId}`);
|
||||||
|
if (!response.ok) return undefined;
|
||||||
|
const data: HFApiBaseModel = await response.json();
|
||||||
|
return data.safetensors?.total;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHuggingFaceSearch = (
|
||||||
|
initialParams: HuggingFaceSearchParams
|
||||||
|
) => {
|
||||||
|
const [models, setModels] = useState<HuggingFaceModelInfo[]>([]);
|
||||||
|
const [files, setFiles] = useState<HuggingFaceFileInfo[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingFiles, setLoadingFiles] = useState(false);
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const [selectedModel, setSelectedModel] = useState<HuggingFaceModelInfo>();
|
||||||
|
const [sortBy, setSortBy] = useState<HuggingFaceSortOption>(
|
||||||
|
initialParams.sort
|
||||||
|
);
|
||||||
|
const [searchParams] = useState<HuggingFaceSearchParams>(initialParams);
|
||||||
|
const pageRef = useRef(0);
|
||||||
|
const currentQueryRef = useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const searchModels = useCallback(
|
||||||
|
async (
|
||||||
|
query?: string,
|
||||||
|
reset = true,
|
||||||
|
sort: HuggingFaceSortOption = sortBy
|
||||||
|
) => {
|
||||||
|
if (reset) {
|
||||||
|
setModels([]);
|
||||||
|
setHasMore(true);
|
||||||
|
setError(undefined);
|
||||||
|
setSelectedModel(undefined);
|
||||||
|
setFiles([]);
|
||||||
|
pageRef.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setSortBy(sort);
|
||||||
|
currentQueryRef.current = query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const searchQuery =
|
||||||
|
query !== undefined ? query.trim() || undefined : searchParams.search;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (searchQuery) params.set('search', searchQuery);
|
||||||
|
if (searchParams.pipelineTag)
|
||||||
|
params.set('pipeline_tag', searchParams.pipelineTag);
|
||||||
|
if (searchParams.filter) params.set('filter', searchParams.filter);
|
||||||
|
params.set('sort', sort);
|
||||||
|
params.set('limit', String(MODELS_PER_PAGE));
|
||||||
|
params.set('full', 'false');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${HF_API_BASE}/models?${params.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: HFApiModel[] = await response.json();
|
||||||
|
const validModels = data.filter((model) => isValidModelId(model.id));
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
validModels.map(async (model) => {
|
||||||
|
const [author, ...nameParts] = model.id.split('/');
|
||||||
|
const name = nameParts.join('/');
|
||||||
|
let paramSize = extractParamSize(name);
|
||||||
|
|
||||||
|
if (!paramSize) {
|
||||||
|
const baseModelId = extractBaseModelId(model.tags);
|
||||||
|
if (baseModelId) {
|
||||||
|
const paramCount = await fetchBaseModelParams(baseModelId);
|
||||||
|
if (paramCount) {
|
||||||
|
paramSize = formatParamCount(paramCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
name,
|
||||||
|
author,
|
||||||
|
downloads: model.downloads ?? 0,
|
||||||
|
likes: model.likes ?? 0,
|
||||||
|
updatedAt: new Date(model.lastModified),
|
||||||
|
gated: model.gated ?? false,
|
||||||
|
paramSize,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setModels(results);
|
||||||
|
setHasMore(data.length === MODELS_PER_PAGE);
|
||||||
|
pageRef.current = 1;
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : 'Failed to search models';
|
||||||
|
setError(message);
|
||||||
|
logError('HuggingFace search failed:', err as Error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[searchParams, sortBy]
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeSortOrder = useCallback(
|
||||||
|
(sort: HuggingFaceSortOption, query?: string) => {
|
||||||
|
searchModels(query, true, sort);
|
||||||
|
},
|
||||||
|
[searchModels]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadMoreModels = useCallback(async () => {
|
||||||
|
if (loading || !hasMore) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const searchQuery =
|
||||||
|
currentQueryRef.current !== undefined
|
||||||
|
? currentQueryRef.current.trim() || undefined
|
||||||
|
: searchParams.search;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (searchQuery) params.set('search', searchQuery);
|
||||||
|
if (searchParams.pipelineTag)
|
||||||
|
params.set('pipeline_tag', searchParams.pipelineTag);
|
||||||
|
if (searchParams.filter) params.set('filter', searchParams.filter);
|
||||||
|
params.set('sort', sortBy);
|
||||||
|
params.set('limit', String(MODELS_PER_PAGE));
|
||||||
|
params.set('skip', String(pageRef.current * MODELS_PER_PAGE));
|
||||||
|
params.set('full', 'false');
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${HF_API_BASE}/models?${params.toString()}`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: HFApiModel[] = await response.json();
|
||||||
|
const validModels = data.filter((model) => isValidModelId(model.id));
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
validModels.map(async (model) => {
|
||||||
|
const [author, ...nameParts] = model.id.split('/');
|
||||||
|
const name = nameParts.join('/');
|
||||||
|
let paramSize = extractParamSize(name);
|
||||||
|
|
||||||
|
if (!paramSize) {
|
||||||
|
const baseModelId = extractBaseModelId(model.tags);
|
||||||
|
if (baseModelId) {
|
||||||
|
const paramCount = await fetchBaseModelParams(baseModelId);
|
||||||
|
if (paramCount) {
|
||||||
|
paramSize = formatParamCount(paramCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
name,
|
||||||
|
author,
|
||||||
|
downloads: model.downloads ?? 0,
|
||||||
|
likes: model.likes ?? 0,
|
||||||
|
updatedAt: new Date(model.lastModified),
|
||||||
|
gated: model.gated ?? false,
|
||||||
|
paramSize,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setModels((prev) => [...prev, ...results]);
|
||||||
|
setHasMore(validModels.length === MODELS_PER_PAGE);
|
||||||
|
pageRef.current += 1;
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : 'Failed to load more models';
|
||||||
|
setError(message);
|
||||||
|
logError('HuggingFace load more failed:', err as Error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [loading, hasMore, searchParams, sortBy]);
|
||||||
|
|
||||||
|
const loadModelFiles = useCallback(
|
||||||
|
async (model: HuggingFaceModelInfo) => {
|
||||||
|
setSelectedModel(model);
|
||||||
|
setFiles([]);
|
||||||
|
setLoadingFiles(true);
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filter = searchParams?.filter;
|
||||||
|
const extensions = getExtensionsForLibrary(filter);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${HF_API_BASE}/models/${model.id}/tree/main?recursive=true`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: HFApiFile[] = await response.json();
|
||||||
|
const fileResults: HuggingFaceFileInfo[] = [];
|
||||||
|
|
||||||
|
for (const file of items) {
|
||||||
|
if (
|
||||||
|
file.type === 'file' &&
|
||||||
|
extensions.some((ext) => file.path.endsWith(ext))
|
||||||
|
) {
|
||||||
|
fileResults.push({
|
||||||
|
path: file.path,
|
||||||
|
size: file.lfs?.size ?? file.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileResults.sort((a, b) => b.size - a.size);
|
||||||
|
setFiles(fileResults);
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : 'Failed to load model files';
|
||||||
|
setError(message);
|
||||||
|
logError('HuggingFace list files failed:', err as Error);
|
||||||
|
} finally {
|
||||||
|
setLoadingFiles(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[searchParams?.filter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFileDownloadUrl = useCallback(
|
||||||
|
(modelId: string, filePath: string) =>
|
||||||
|
`${HUGGINGFACE_BASE_URL}/${modelId}/resolve/main/${filePath}`,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setModels([]);
|
||||||
|
setFiles([]);
|
||||||
|
setSelectedModel(undefined);
|
||||||
|
setError(undefined);
|
||||||
|
setHasMore(true);
|
||||||
|
pageRef.current = 0;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
models,
|
||||||
|
files,
|
||||||
|
loading,
|
||||||
|
loadingFiles,
|
||||||
|
error,
|
||||||
|
hasMore,
|
||||||
|
selectedModel,
|
||||||
|
sortBy,
|
||||||
|
searchParams,
|
||||||
|
searchModels,
|
||||||
|
loadMoreModels,
|
||||||
|
loadModelFiles,
|
||||||
|
changeSortOrder,
|
||||||
|
getFileDownloadUrl,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExtensionsForLibrary = (filter?: string) => {
|
||||||
|
switch (filter) {
|
||||||
|
case 'gguf':
|
||||||
|
return ['.gguf'];
|
||||||
|
case 'safetensors':
|
||||||
|
return ['.safetensors'];
|
||||||
|
default:
|
||||||
|
return ['.gguf', '.safetensors', '.bin', '.pt', '.pth'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -3,7 +3,7 @@ import { readdir, stat, unlink } from 'fs/promises';
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
|
||||||
import { getInstallDir, setInstallDir } from '../config';
|
import { getInstallDir, setInstallDir } from '../config';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||||
import { getMainWindow, sendToRenderer } from '../window';
|
import { getMainWindow, sendToRenderer } from '../window';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
|
@ -43,48 +43,37 @@ export async function getConfigFiles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseConfigFile(filePath: string) {
|
export async function parseConfigFile(filePath: string) {
|
||||||
try {
|
return safeExecute(async () => {
|
||||||
if (!(await pathExists(filePath))) {
|
if (!(await pathExists(filePath))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await readJsonFile(filePath);
|
const config = await readJsonFile(filePath);
|
||||||
return config as KoboldConfig;
|
return config as KoboldConfig;
|
||||||
} catch (error) {
|
}, 'Error parsing config file');
|
||||||
logError('Error parsing config file:', error as Error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveConfigFile(
|
export async function saveConfigFile(
|
||||||
configFileName: string,
|
configFileName: string,
|
||||||
configData: KoboldConfig
|
configData: KoboldConfig
|
||||||
) {
|
) {
|
||||||
try {
|
return tryExecute(async () => {
|
||||||
const installDir = getInstallDir();
|
const installDir = getInstallDir();
|
||||||
const configPath = join(installDir, configFileName);
|
const configPath = join(installDir, configFileName);
|
||||||
await writeJsonFile(configPath, configData);
|
await writeJsonFile(configPath, configData);
|
||||||
return true;
|
}, 'Error saving config file');
|
||||||
} catch (error) {
|
|
||||||
logError('Error saving config file:', error as Error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteConfigFile(configFileName: string) {
|
export async function deleteConfigFile(configFileName: string) {
|
||||||
try {
|
return tryExecute(async () => {
|
||||||
const installDir = getInstallDir();
|
const installDir = getInstallDir();
|
||||||
const configPath = join(installDir, configFileName);
|
const configPath = join(installDir, configFileName);
|
||||||
await unlink(configPath);
|
await unlink(configPath);
|
||||||
return true;
|
}, 'Error deleting config file');
|
||||||
} catch (error) {
|
|
||||||
logError('Error deleting config file:', error as Error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectModelFile(title = 'Select Model File') {
|
export async function selectModelFile(title = 'Select Model File') {
|
||||||
try {
|
return safeExecute(async () => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -107,10 +96,7 @@ export async function selectModelFile(title = 'Select Model File') {
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
} catch (error) {
|
}, 'Error selecting model file');
|
||||||
logError('Error selecting model file:', error as Error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectInstallDirectory() {
|
export async function selectInstallDirectory() {
|
||||||
|
|
|
||||||
29
src/types/index.d.ts
vendored
29
src/types/index.d.ts
vendored
|
|
@ -125,3 +125,32 @@ export interface ModelAnalysis {
|
||||||
vramPerLayer?: string;
|
vramPerLayer?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HuggingFaceModelInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
author: string;
|
||||||
|
downloads: number;
|
||||||
|
likes: number;
|
||||||
|
updatedAt: Date;
|
||||||
|
gated: boolean | 'auto' | 'manual';
|
||||||
|
paramSize?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HuggingFaceFileInfo {
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HuggingFaceSearchParams {
|
||||||
|
search?: string;
|
||||||
|
pipelineTag?: string;
|
||||||
|
filter?: string;
|
||||||
|
sort: HuggingFaceSortOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HuggingFaceSortOption =
|
||||||
|
| 'trendingScore'
|
||||||
|
| 'downloads'
|
||||||
|
| 'likes'
|
||||||
|
| 'lastModified';
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,22 @@ export const formatDeviceName = (deviceName: string) =>
|
||||||
|
|
||||||
export const stripFileExtension = (filename: string) =>
|
export const stripFileExtension = (filename: string) =>
|
||||||
filename.replace(/\.[^/.]+$/, '');
|
filename.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
export const formatDownloads = (count: number) => {
|
||||||
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
|
||||||
|
return count.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (date: Date) => {
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (days === 0) return 'today';
|
||||||
|
if (days === 1) return 'yesterday';
|
||||||
|
if (days < 7) return `${days}d ago`;
|
||||||
|
if (days < 30) return `${Math.floor(days / 7)}w ago`;
|
||||||
|
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
|
||||||
|
return `${Math.floor(days / 365)}y ago`;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue