mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
fix the backends tab to work in offline mode and allow it to import new backend binaries
This commit is contained in:
parent
d5fbf1df35
commit
f825cd0d2f
5 changed files with 125 additions and 102 deletions
24
.github/copilot-instructions.md
vendored
24
.github/copilot-instructions.md
vendored
|
|
@ -1,21 +1,7 @@
|
|||
# Copilot Instructions for Gerbil
|
||||
|
||||
### General Coding
|
||||
|
||||
- Follow existing TypeScript/React patterns in the codebase
|
||||
- Maintain consistency with the established project structure
|
||||
- Use proper TypeScript types (avoid `any` when possible)
|
||||
- Follow the ESLint configuration (includes SonarJS and security rules)
|
||||
- Never create tests, docs or github workflows
|
||||
- Stop asking me to run the "dev" script to test changes
|
||||
- Try to move helper functions from component code to their own separate files to help minimize clutter
|
||||
- Always use absolute imports (e.g. `import { MyComponent } from '@/components/MyComponent'`)
|
||||
- Never add explicit return types to functions. We want to rely on implicit types as much as possible
|
||||
|
||||
### Logging and Error Handling
|
||||
|
||||
- **NEVER use console.\* calls** (console.log, console.error, console.warn, etc.) - they are blocked by ESLint
|
||||
- **Backend errors**: Use `this.logManager.logError(message, error)` for main process errors
|
||||
- **Frontend errors**: Use `window.electronAPI.logs.logError(message, error)` for renderer process errors
|
||||
- All errors are logged asynchronously to avoid blocking the event loop
|
||||
- Only use console.\* for critical system errors that must appear in terminal (very rare cases)
|
||||
- **NEVER use console.\* calls** - they are blocked by ESLint. Use `logError()` from `@/utils/node/logging` (main process) or `window.electronAPI.logs.logError()` (renderer)
|
||||
- Always use absolute imports: `import { X } from '@/components/X'`
|
||||
- Never add explicit return types to functions - rely on TypeScript inference
|
||||
- Never create tests, docs, or GitHub workflows
|
||||
- Move helper functions out of component files into separate utility files
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"productName": "Gerbil",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.1",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
|
|||
63
src/components/ImportBackendLink.tsx
Normal file
63
src/components/ImportBackendLink.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { useState } from 'react';
|
||||
import { Text, Anchor } from '@mantine/core';
|
||||
|
||||
interface ImportBackendLinkProps {
|
||||
disabled?: boolean;
|
||||
onSuccess: () => void | Promise<void>;
|
||||
onImportingChange?: (importing: boolean) => void;
|
||||
}
|
||||
|
||||
export const ImportBackendLink = ({
|
||||
disabled = false,
|
||||
onSuccess,
|
||||
onImportingChange,
|
||||
}: ImportBackendLinkProps) => {
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
const updateImporting = (value: boolean) => {
|
||||
setImporting(value);
|
||||
onImportingChange?.(value);
|
||||
};
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
|
||||
const isDisabled = disabled || importing;
|
||||
|
||||
const handleImport = async () => {
|
||||
setImportError(null);
|
||||
updateImporting(true);
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.kobold.importLocalBackend();
|
||||
|
||||
if (result.success) {
|
||||
await onSuccess();
|
||||
} else if (result.error) {
|
||||
setImportError(result.error);
|
||||
}
|
||||
} finally {
|
||||
updateImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{importError && (
|
||||
<Text size="sm" c="red" ta="center" mb="xs">
|
||||
{importError}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
Already have a backend downloaded?{' '}
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={isDisabled}
|
||||
onClick={handleImport}
|
||||
>
|
||||
{importing ? 'Importing...' : 'Select a local file'}
|
||||
</Anchor>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,14 +1,7 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Title,
|
||||
Loader,
|
||||
Stack,
|
||||
Container,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
||||
import { getPlatformDisplayName } from '@/utils/platform';
|
||||
import { formatDownloadSize } from '@/utils/format';
|
||||
import { getAssetDescription } from '@/utils/assets';
|
||||
|
|
@ -30,7 +23,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
} = useKoboldBackendsStore();
|
||||
|
||||
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -123,40 +115,11 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
</Text>
|
||||
)}
|
||||
|
||||
{importError && (
|
||||
<Text size="sm" c="red" ta="center">
|
||||
{importError}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
Already have a backend downloaded?{' '}
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={importing || Boolean(downloading)}
|
||||
onClick={async () => {
|
||||
setImportError(null);
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
const result =
|
||||
await window.electronAPI.kobold.importLocalBackend();
|
||||
|
||||
if (result.success) {
|
||||
onDownloadComplete();
|
||||
} else if (result.error) {
|
||||
setImportError(result.error);
|
||||
}
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{importing ? 'Importing...' : 'Select a local file'}
|
||||
</Anchor>
|
||||
</Text>
|
||||
<ImportBackendLink
|
||||
disabled={Boolean(downloading)}
|
||||
onSuccess={onDownloadComplete}
|
||||
onImportingChange={setImporting}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import {
|
|||
Loader,
|
||||
Center,
|
||||
Anchor,
|
||||
Divider,
|
||||
} from '@mantine/core';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
||||
import { getAssetDescription } from '@/utils/assets';
|
||||
import {
|
||||
getDisplayNameFromPath,
|
||||
|
|
@ -30,7 +32,6 @@ export const BackendsTab = () => {
|
|||
downloading,
|
||||
handleDownload: handleDownloadFromStore,
|
||||
getLatestReleaseWithDownloadStatus,
|
||||
initialize,
|
||||
} = useKoboldBackendsStore();
|
||||
|
||||
const [installedBackends, setInstalledBackends] = useState<
|
||||
|
|
@ -43,9 +44,11 @@ export const BackendsTab = () => {
|
|||
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
|
||||
null
|
||||
);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadInstalledBackends = useCallback(async () => {
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
setLoadingInstalled(true);
|
||||
|
||||
const [backends, current] = await Promise.all([
|
||||
|
|
@ -55,34 +58,27 @@ export const BackendsTab = () => {
|
|||
|
||||
setInstalledBackends(backends);
|
||||
setCurrentBackend(current);
|
||||
|
||||
setLoadingInstalled(false);
|
||||
}, []);
|
||||
|
||||
const loadLatestRelease = useCallback(async () => {
|
||||
const release = await getLatestReleaseWithDownloadStatus();
|
||||
if (release) {
|
||||
setLatestRelease(release);
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
}, [getLatestReleaseWithDownloadStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (availableDownloads.length === 0 && !loadingRemote && !loadingPlatform) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
loadInstalledBackends();
|
||||
loadLatestRelease();
|
||||
}, [
|
||||
loadInstalledBackends,
|
||||
loadLatestRelease,
|
||||
availableDownloads.length,
|
||||
loadingRemote,
|
||||
loadingPlatform,
|
||||
initialize,
|
||||
const loadInstalledBackends = useCallback(async () => {
|
||||
const [backends, current] = await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledBackends(),
|
||||
window.electronAPI.kobold.getCurrentBackend(),
|
||||
]);
|
||||
|
||||
setInstalledBackends(backends);
|
||||
setCurrentBackend(current);
|
||||
}, []);
|
||||
|
||||
const allBackends = useMemo((): BackendInfo[] => {
|
||||
const backends: BackendInfo[] = [];
|
||||
const processedInstalled = new Set<string>();
|
||||
|
|
@ -235,23 +231,19 @@ export const BackendsTab = () => {
|
|||
window.electronAPI.kobold.setCurrentBackend(backend.installedPath);
|
||||
};
|
||||
|
||||
if (loadingInstalled || loadingPlatform || loadingRemote) {
|
||||
if (loadingInstalled) {
|
||||
return (
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="md">
|
||||
<Loader size="lg" />
|
||||
<Text c="dimmed">
|
||||
{loadingInstalled && (loadingPlatform || loadingRemote)
|
||||
? 'Loading backends...'
|
||||
: loadingInstalled
|
||||
? 'Scanning installed backends...'
|
||||
: 'Checking for updates...'}
|
||||
</Text>
|
||||
<Text c="dimmed">Scanning installed backends...</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
const isDisabled = downloading !== null || importing;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group justify="space-between" align="center" mb="sm">
|
||||
|
|
@ -276,6 +268,17 @@ export const BackendsTab = () => {
|
|||
)}
|
||||
</Group>
|
||||
|
||||
{(loadingPlatform || loadingRemote) && (
|
||||
<Card withBorder radius="md" padding="md" mb="sm">
|
||||
<Group gap="sm" justify="center">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Checking for available downloads...
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{allBackends.map((backend, index) => {
|
||||
const isDownloading = downloading === backend.name;
|
||||
|
||||
|
|
@ -293,7 +296,7 @@ export const BackendsTab = () => {
|
|||
: ''
|
||||
}
|
||||
description={getAssetDescription(backend.name)}
|
||||
disabled={downloading !== null}
|
||||
disabled={isDisabled}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload(backend);
|
||||
|
|
@ -323,6 +326,14 @@ export const BackendsTab = () => {
|
|||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<ImportBackendLink
|
||||
disabled={isDisabled}
|
||||
onSuccess={loadInstalledBackends}
|
||||
onImportingChange={setImporting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue