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
|
# Copilot Instructions for Gerbil
|
||||||
|
|
||||||
### General Coding
|
- **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'`
|
||||||
- Follow existing TypeScript/React patterns in the codebase
|
- Never add explicit return types to functions - rely on TypeScript inference
|
||||||
- Maintain consistency with the established project structure
|
- Never create tests, docs, or GitHub workflows
|
||||||
- Use proper TypeScript types (avoid `any` when possible)
|
- Move helper functions out of component files into separate utility files
|
||||||
- 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)
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "1.14.0",
|
"version": "1.14.1",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"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 { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import {
|
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
|
||||||
Card,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
Loader,
|
|
||||||
Stack,
|
|
||||||
Container,
|
|
||||||
Anchor,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
|
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
||||||
import { getPlatformDisplayName } from '@/utils/platform';
|
import { getPlatformDisplayName } from '@/utils/platform';
|
||||||
import { formatDownloadSize } from '@/utils/format';
|
import { formatDownloadSize } from '@/utils/format';
|
||||||
import { getAssetDescription } from '@/utils/assets';
|
import { getAssetDescription } from '@/utils/assets';
|
||||||
|
|
@ -30,7 +23,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
} = useKoboldBackendsStore();
|
} = useKoboldBackendsStore();
|
||||||
|
|
||||||
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
|
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
|
||||||
const [importError, setImportError] = useState<string | null>(null);
|
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -123,40 +115,11 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{importError && (
|
<ImportBackendLink
|
||||||
<Text size="sm" c="red" ta="center">
|
disabled={Boolean(downloading)}
|
||||||
{importError}
|
onSuccess={onDownloadComplete}
|
||||||
</Text>
|
onImportingChange={setImporting}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ import {
|
||||||
Loader,
|
Loader,
|
||||||
Center,
|
Center,
|
||||||
Anchor,
|
Anchor,
|
||||||
|
Divider,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
|
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
||||||
import { getAssetDescription } from '@/utils/assets';
|
import { getAssetDescription } from '@/utils/assets';
|
||||||
import {
|
import {
|
||||||
getDisplayNameFromPath,
|
getDisplayNameFromPath,
|
||||||
|
|
@ -30,7 +32,6 @@ export const BackendsTab = () => {
|
||||||
downloading,
|
downloading,
|
||||||
handleDownload: handleDownloadFromStore,
|
handleDownload: handleDownloadFromStore,
|
||||||
getLatestReleaseWithDownloadStatus,
|
getLatestReleaseWithDownloadStatus,
|
||||||
initialize,
|
|
||||||
} = useKoboldBackendsStore();
|
} = useKoboldBackendsStore();
|
||||||
|
|
||||||
const [installedBackends, setInstalledBackends] = useState<
|
const [installedBackends, setInstalledBackends] = useState<
|
||||||
|
|
@ -43,11 +44,32 @@ export const BackendsTab = () => {
|
||||||
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
|
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [importing, setImporting] = useState(false);
|
||||||
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const loadInstalledBackends = useCallback(async () => {
|
useEffect(() => {
|
||||||
setLoadingInstalled(true);
|
const init = async () => {
|
||||||
|
setLoadingInstalled(true);
|
||||||
|
|
||||||
|
const [backends, current] = await Promise.all([
|
||||||
|
window.electronAPI.kobold.getInstalledBackends(),
|
||||||
|
window.electronAPI.kobold.getCurrentBackend(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setInstalledBackends(backends);
|
||||||
|
setCurrentBackend(current);
|
||||||
|
setLoadingInstalled(false);
|
||||||
|
|
||||||
|
const release = await getLatestReleaseWithDownloadStatus();
|
||||||
|
if (release) {
|
||||||
|
setLatestRelease(release);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, [getLatestReleaseWithDownloadStatus]);
|
||||||
|
|
||||||
|
const loadInstalledBackends = useCallback(async () => {
|
||||||
const [backends, current] = await Promise.all([
|
const [backends, current] = await Promise.all([
|
||||||
window.electronAPI.kobold.getInstalledBackends(),
|
window.electronAPI.kobold.getInstalledBackends(),
|
||||||
window.electronAPI.kobold.getCurrentBackend(),
|
window.electronAPI.kobold.getCurrentBackend(),
|
||||||
|
|
@ -55,34 +77,8 @@ export const BackendsTab = () => {
|
||||||
|
|
||||||
setInstalledBackends(backends);
|
setInstalledBackends(backends);
|
||||||
setCurrentBackend(current);
|
setCurrentBackend(current);
|
||||||
|
|
||||||
setLoadingInstalled(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadLatestRelease = useCallback(async () => {
|
|
||||||
const release = await getLatestReleaseWithDownloadStatus();
|
|
||||||
if (release) {
|
|
||||||
setLatestRelease(release);
|
|
||||||
}
|
|
||||||
}, [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 allBackends = useMemo((): BackendInfo[] => {
|
const allBackends = useMemo((): BackendInfo[] => {
|
||||||
const backends: BackendInfo[] = [];
|
const backends: BackendInfo[] = [];
|
||||||
const processedInstalled = new Set<string>();
|
const processedInstalled = new Set<string>();
|
||||||
|
|
@ -235,23 +231,19 @@ export const BackendsTab = () => {
|
||||||
window.electronAPI.kobold.setCurrentBackend(backend.installedPath);
|
window.electronAPI.kobold.setCurrentBackend(backend.installedPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loadingInstalled || loadingPlatform || loadingRemote) {
|
if (loadingInstalled) {
|
||||||
return (
|
return (
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Loader size="lg" />
|
<Loader size="lg" />
|
||||||
<Text c="dimmed">
|
<Text c="dimmed">Scanning installed backends...</Text>
|
||||||
{loadingInstalled && (loadingPlatform || loadingRemote)
|
|
||||||
? 'Loading backends...'
|
|
||||||
: loadingInstalled
|
|
||||||
? 'Scanning installed backends...'
|
|
||||||
: 'Checking for updates...'}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDisabled = downloading !== null || importing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Group justify="space-between" align="center" mb="sm">
|
<Group justify="space-between" align="center" mb="sm">
|
||||||
|
|
@ -276,6 +268,17 @@ export const BackendsTab = () => {
|
||||||
)}
|
)}
|
||||||
</Group>
|
</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) => {
|
{allBackends.map((backend, index) => {
|
||||||
const isDownloading = downloading === backend.name;
|
const isDownloading = downloading === backend.name;
|
||||||
|
|
||||||
|
|
@ -293,7 +296,7 @@ export const BackendsTab = () => {
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
description={getAssetDescription(backend.name)}
|
description={getAssetDescription(backend.name)}
|
||||||
disabled={downloading !== null}
|
disabled={isDisabled}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload(backend);
|
handleDownload(backend);
|
||||||
|
|
@ -323,6 +326,14 @@ export const BackendsTab = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Divider my="md" />
|
||||||
|
|
||||||
|
<ImportBackendLink
|
||||||
|
disabled={isDisabled}
|
||||||
|
onSuccess={loadInstalledBackends}
|
||||||
|
onImportingChange={setImporting}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue