mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
unpack the binary before working with it, bug fixes, 2 more advanced configuration items
This commit is contained in:
parent
01058a8d02
commit
21b6f87a53
21 changed files with 964 additions and 443 deletions
|
|
@ -8,6 +8,7 @@ A koboldcpp manager.
|
||||||
- download and keep up-to-date your [koboldcpp](https://github.com/LostRuins/koboldcpp/releases) binary
|
- download and keep up-to-date your [koboldcpp](https://github.com/LostRuins/koboldcpp/releases) binary
|
||||||
- better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm)
|
- better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm)
|
||||||
- manage the koboldcpp binary to prevent it from running in the background indefinitely
|
- manage the koboldcpp binary to prevent it from running in the background indefinitely
|
||||||
|
- automatically unpack all downloaded koboldcpp binaries for significantly faster operations
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
|
|
|
||||||
19
cspell.json
19
cspell.json
|
|
@ -22,10 +22,10 @@
|
||||||
"bundler",
|
"bundler",
|
||||||
"bundling",
|
"bundling",
|
||||||
"can",
|
"can",
|
||||||
"clblast",
|
|
||||||
"clinfo",
|
|
||||||
"classList",
|
"classList",
|
||||||
"className",
|
"className",
|
||||||
|
"clblast",
|
||||||
|
"clinfo",
|
||||||
"cloneNode",
|
"cloneNode",
|
||||||
"codeinterface",
|
"codeinterface",
|
||||||
"componentDidCatch",
|
"componentDidCatch",
|
||||||
|
|
@ -60,11 +60,11 @@
|
||||||
"filenames",
|
"filenames",
|
||||||
"filepath",
|
"filepath",
|
||||||
"filepaths",
|
"filepaths",
|
||||||
|
"flashattention",
|
||||||
|
"Flashattention",
|
||||||
"flexbox",
|
"flexbox",
|
||||||
"flexdir",
|
"flexdir",
|
||||||
"flexwrap",
|
"flexwrap",
|
||||||
"flashattention",
|
|
||||||
"Flashattention",
|
|
||||||
"fontsize",
|
"fontsize",
|
||||||
"fontweight",
|
"fontweight",
|
||||||
"forwardRef",
|
"forwardRef",
|
||||||
|
|
@ -118,11 +118,12 @@
|
||||||
"minified",
|
"minified",
|
||||||
"minify",
|
"minify",
|
||||||
"moz",
|
"moz",
|
||||||
"multiuser",
|
|
||||||
"multiplayer",
|
"multiplayer",
|
||||||
|
"multiuser",
|
||||||
"namespace",
|
"namespace",
|
||||||
"namespaces",
|
"namespaces",
|
||||||
"newpackagename",
|
"newpackagename",
|
||||||
|
"noavx",
|
||||||
"nocertify",
|
"nocertify",
|
||||||
"nocuda",
|
"nocuda",
|
||||||
"nodeIntegration",
|
"nodeIntegration",
|
||||||
|
|
@ -150,6 +151,8 @@
|
||||||
"preload",
|
"preload",
|
||||||
"prettierrc",
|
"prettierrc",
|
||||||
"preventDefault",
|
"preventDefault",
|
||||||
|
"PYTHONDONTWRITEBYTECODE",
|
||||||
|
"PYTHONUNBUFFERED",
|
||||||
"querySelector",
|
"querySelector",
|
||||||
"querySelectorAll",
|
"querySelectorAll",
|
||||||
"radeon",
|
"radeon",
|
||||||
|
|
@ -208,14 +211,14 @@
|
||||||
"url",
|
"url",
|
||||||
"urls",
|
"urls",
|
||||||
"useCallback",
|
"useCallback",
|
||||||
|
"useclblast",
|
||||||
"useContext",
|
"useContext",
|
||||||
|
"usecuda",
|
||||||
"useEffect",
|
"useEffect",
|
||||||
"useMemo",
|
"useMemo",
|
||||||
"useReducer",
|
"useReducer",
|
||||||
"useRef",
|
"useRef",
|
||||||
"useState",
|
"useState",
|
||||||
"useclblast",
|
|
||||||
"usecuda",
|
|
||||||
"usevulkan",
|
"usevulkan",
|
||||||
"util",
|
"util",
|
||||||
"utils",
|
"utils",
|
||||||
|
|
@ -231,8 +234,8 @@
|
||||||
"webkit",
|
"webkit",
|
||||||
"webp",
|
"webp",
|
||||||
"webpack",
|
"webpack",
|
||||||
"webSecurity",
|
|
||||||
"websearch",
|
"websearch",
|
||||||
|
"webSecurity",
|
||||||
"whitespace",
|
"whitespace",
|
||||||
"wmic",
|
"wmic",
|
||||||
"woff",
|
"woff",
|
||||||
|
|
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -29,7 +29,7 @@
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"cspell": "^9.2.0",
|
"cspell": "^9.2.0",
|
||||||
"electron": "^37.2.6",
|
"electron": "^37.3.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.33.0",
|
||||||
|
|
@ -5607,9 +5607,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
"version": "37.2.6",
|
"version": "37.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-37.3.0.tgz",
|
||||||
"integrity": "sha512-Ns6xyxE+hIK5UlujtRlw7w4e2Ju/ImCWXf1Q/PoOhc0N3/6SN6YW7+ujCarsHbxWnolbW+1RlkHtdklUJpjbPA==",
|
"integrity": "sha512-cPOPUD26DwCh+PZ9q+gMyVBvdBN75SnekI6u5zcOeoLVIXQpzrCm1ewz9BcrkWkVW7oOtfQAEo1G1SffvXrSSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"cspell": "^9.2.0",
|
"cspell": "^9.2.0",
|
||||||
"electron": "^37.2.6",
|
"electron": "^37.3.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.33.0",
|
||||||
|
|
|
||||||
166
src/App.tsx
166
src/App.tsx
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
Button,
|
Button,
|
||||||
Select,
|
Select,
|
||||||
|
Badge,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Settings, ArrowLeft } from 'lucide-react';
|
import { Settings, ArrowLeft } from 'lucide-react';
|
||||||
|
|
@ -20,7 +21,7 @@ import { InterfaceScreen } from '@/components/screens/InterfaceScreen';
|
||||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||||
import { SettingsModal } from '@/components/SettingsModal';
|
import { SettingsModal } from '@/components/SettingsModal';
|
||||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||||
import type { UpdateInfo } from '@/types';
|
import type { UpdateInfo, InstalledVersion } from '@/types';
|
||||||
|
|
||||||
type Screen = 'download' | 'launch' | 'interface';
|
type Screen = 'download' | 'launch' | 'interface';
|
||||||
|
|
||||||
|
|
@ -33,24 +34,64 @@ export const App = () => {
|
||||||
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
|
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
|
||||||
'terminal'
|
'terminal'
|
||||||
);
|
);
|
||||||
|
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [installedVersions, setInstalledVersions] = useState<
|
||||||
|
InstalledVersion[]
|
||||||
|
>([]);
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
const getDisplayNameFromPath = (
|
||||||
|
installedVersion: InstalledVersion
|
||||||
|
): string => {
|
||||||
|
const pathParts = installedVersion.path.split(/[/\\]/);
|
||||||
|
const launcherIndex = pathParts.findIndex(
|
||||||
|
(part) =>
|
||||||
|
part === 'koboldcpp-launcher' ||
|
||||||
|
part === 'koboldcpp.exe' ||
|
||||||
|
part === 'koboldcpp'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launcherIndex > 0) {
|
||||||
|
return pathParts[launcherIndex - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return installedVersion.filename;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkInstallation = async () => {
|
const checkInstallation = async () => {
|
||||||
try {
|
try {
|
||||||
const startTime = Date.now();
|
const [versions, currentBinaryPath] = await Promise.all([
|
||||||
const installedVersions =
|
window.electronAPI.kobold.getInstalledVersions(),
|
||||||
await window.electronAPI.kobold.getInstalledVersions(false);
|
window.electronAPI.config.get(
|
||||||
|
'currentKoboldBinary'
|
||||||
|
) as Promise<string>,
|
||||||
|
]);
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
setInstalledVersions(versions);
|
||||||
const minDelay = 500;
|
|
||||||
if (elapsed < minDelay) {
|
if (versions.length > 0) {
|
||||||
await new Promise((resolve) =>
|
let current = null;
|
||||||
setTimeout(resolve, minDelay - elapsed)
|
if (currentBinaryPath) {
|
||||||
);
|
current = versions.find((v) => v.path === currentBinaryPath);
|
||||||
|
}
|
||||||
|
if (!current) {
|
||||||
|
current = versions[0];
|
||||||
|
if (current) {
|
||||||
|
await window.electronAPI.config.set(
|
||||||
|
'currentKoboldBinary',
|
||||||
|
current.path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentVersion(current);
|
||||||
|
setCurrentScreen('launch');
|
||||||
|
} else {
|
||||||
|
setCurrentScreen('download');
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentScreen(installedVersions.length > 0 ? 'launch' : 'download');
|
|
||||||
setHasInitialized(true);
|
setHasInitialized(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking installation:', error);
|
console.error('Error checking installation:', error);
|
||||||
|
|
@ -70,18 +111,73 @@ export const App = () => {
|
||||||
checkInstallation();
|
checkInstallation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cleanupVersionsListener = window.electronAPI.kobold.onVersionsUpdated(
|
||||||
|
() => {
|
||||||
|
checkInstallation();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.electronAPI.kobold.removeAllListeners('update-available');
|
window.electronAPI.kobold.removeAllListeners('update-available');
|
||||||
cleanupInstallDirListener();
|
cleanupInstallDirListener();
|
||||||
|
cleanupVersionsListener();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDownloadComplete = () => {
|
const handleDownloadComplete = async () => {
|
||||||
|
try {
|
||||||
|
const [versions, currentBinaryPath] = await Promise.all([
|
||||||
|
window.electronAPI.kobold.getInstalledVersions(),
|
||||||
|
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
setInstalledVersions(versions);
|
||||||
|
|
||||||
|
if (versions.length > 0) {
|
||||||
|
let current = null;
|
||||||
|
if (currentBinaryPath) {
|
||||||
|
current = versions.find((v) => v.path === currentBinaryPath);
|
||||||
|
}
|
||||||
|
if (!current) {
|
||||||
|
current = versions[0];
|
||||||
|
if (current) {
|
||||||
|
await window.electronAPI.config.set(
|
||||||
|
'currentKoboldBinary',
|
||||||
|
current.path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentVersion(current);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing versions after download:', error);
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCurrentScreen('launch');
|
setCurrentScreen('launch');
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVersionChange = async (versionPath: string | null) => {
|
||||||
|
if (!versionPath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success =
|
||||||
|
await window.electronAPI.kobold.setCurrentVersion(versionPath);
|
||||||
|
if (success) {
|
||||||
|
await window.electronAPI.config.set('currentKoboldBinary', versionPath);
|
||||||
|
const newCurrent = installedVersions.find(
|
||||||
|
(v) => v.path === versionPath
|
||||||
|
);
|
||||||
|
if (newCurrent) {
|
||||||
|
setCurrentVersion(newCurrent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to change version:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLaunch = () => {
|
const handleLaunch = () => {
|
||||||
setCurrentScreen('interface');
|
setCurrentScreen('interface');
|
||||||
};
|
};
|
||||||
|
|
@ -170,6 +266,49 @@ export const App = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentScreen === 'launch' && currentVersion && (
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Select
|
||||||
|
value={currentVersion.path}
|
||||||
|
onChange={handleVersionChange}
|
||||||
|
data={installedVersions.map((version) => ({
|
||||||
|
value: version.path,
|
||||||
|
label: getDisplayNameFromPath(version),
|
||||||
|
}))}
|
||||||
|
placeholder="Select version"
|
||||||
|
variant="unstyled"
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
minWidth: '225px',
|
||||||
|
textAlign: 'center',
|
||||||
|
border: `1px solid var(--mantine-color-${colorScheme === 'dark' ? 'dark-4' : 'gray-4'})`,
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
backgroundColor:
|
||||||
|
colorScheme === 'dark'
|
||||||
|
? 'var(--mantine-color-dark-6)'
|
||||||
|
: 'var(--mantine-color-gray-0)',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '14px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
transition: 'all 200ms ease',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
colorScheme === 'dark'
|
||||||
|
? 'var(--mantine-color-dark-5)'
|
||||||
|
: 'var(--mantine-color-gray-1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
minWidth: '200px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Badge variant="light" color="blue" size="sm">
|
||||||
|
v{currentVersion.version}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
minWidth: '100px',
|
minWidth: '100px',
|
||||||
|
|
@ -206,7 +345,7 @@ export const App = () => {
|
||||||
<Stack align="center" gap="lg">
|
<Stack align="center" gap="lg">
|
||||||
<Loader size="xl" type="dots" />
|
<Loader size="xl" type="dots" />
|
||||||
<Text c="dimmed" size="lg">
|
<Text c="dimmed" size="lg">
|
||||||
Initializing...
|
Loading...
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
@ -249,6 +388,7 @@ export const App = () => {
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
opened={settingsOpened}
|
opened={settingsOpened}
|
||||||
onClose={() => setSettingsOpened(false)}
|
onClose={() => setSettingsOpened(false)}
|
||||||
|
currentScreen={currentScreen || undefined}
|
||||||
/>
|
/>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
143
src/components/DownloadCard.tsx
Normal file
143
src/components/DownloadCard.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Loader,
|
||||||
|
Progress,
|
||||||
|
rem,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
|
interface DownloadCardProps {
|
||||||
|
name: string;
|
||||||
|
size: string;
|
||||||
|
version?: string;
|
||||||
|
description?: string;
|
||||||
|
isRecommended?: boolean;
|
||||||
|
isCurrent?: boolean;
|
||||||
|
isInstalled?: boolean;
|
||||||
|
isDownloading?: boolean;
|
||||||
|
downloadProgress?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onMakeCurrent?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DownloadCard = ({
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
isRecommended = false,
|
||||||
|
isCurrent = false,
|
||||||
|
isInstalled = false,
|
||||||
|
isDownloading = false,
|
||||||
|
downloadProgress = 0,
|
||||||
|
disabled = false,
|
||||||
|
onDownload,
|
||||||
|
onMakeCurrent,
|
||||||
|
}: DownloadCardProps) => {
|
||||||
|
const renderActionButton = () => {
|
||||||
|
if (!isInstalled && onDownload) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
size="xs"
|
||||||
|
onClick={onDownload}
|
||||||
|
loading={isDownloading}
|
||||||
|
disabled={disabled}
|
||||||
|
leftSection={
|
||||||
|
isDownloading ? (
|
||||||
|
<Loader size="1rem" />
|
||||||
|
) : (
|
||||||
|
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isDownloading ? 'Downloading...' : 'Download'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInstalled && !isCurrent && onMakeCurrent) {
|
||||||
|
return (
|
||||||
|
<Button variant="light" size="xs" onClick={onMakeCurrent}>
|
||||||
|
Make Current
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="sm"
|
||||||
|
padding="sm"
|
||||||
|
bd={isCurrent ? '2px solid var(--mantine-color-blue-filled)' : undefined}
|
||||||
|
bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{isCurrent && (
|
||||||
|
<Badge variant="light" color="blue" size="sm">
|
||||||
|
Current
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isRecommended && (
|
||||||
|
<Badge variant="light" color="blue" size="sm">
|
||||||
|
Recommended
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
{description && (
|
||||||
|
<Text size="xs" c="dimmed" mb="xs">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
{version && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Version {version}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{size && (
|
||||||
|
<>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Badge variant="light" color="gray" size="xs">
|
||||||
|
{size}
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderActionButton()}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{isDownloading && downloadProgress !== undefined && (
|
||||||
|
<Stack gap="xs" mt="sm">
|
||||||
|
<Progress
|
||||||
|
value={Math.min(downloadProgress, 100)}
|
||||||
|
color="blue"
|
||||||
|
radius="xl"
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="dimmed" ta="center">
|
||||||
|
{Math.min(downloadProgress, 100).toFixed(1)}% complete
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Stack,
|
|
||||||
Group,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Loader,
|
|
||||||
Progress,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Download } from 'lucide-react';
|
|
||||||
import { MouseEvent } from 'react';
|
|
||||||
|
|
||||||
interface DownloadOptionCardProps {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
size: string;
|
|
||||||
isSelected: boolean;
|
|
||||||
isRecommended: boolean;
|
|
||||||
isDownloading: boolean;
|
|
||||||
downloadProgress?: number;
|
|
||||||
onClick: () => void;
|
|
||||||
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DownloadOptionCard = ({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
size,
|
|
||||||
isSelected,
|
|
||||||
isRecommended,
|
|
||||||
isDownloading,
|
|
||||||
downloadProgress = 0,
|
|
||||||
onClick,
|
|
||||||
onDownload,
|
|
||||||
}: DownloadOptionCardProps) => (
|
|
||||||
<Card
|
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
bd={isSelected ? '2px solid var(--mantine-color-blue-filled)' : undefined}
|
|
||||||
bg={isSelected ? 'var(--mantine-color-blue-light)' : undefined}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<Stack gap="xs" style={{ flex: 1 }}>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text fw={500}>{name}</Text>
|
|
||||||
{isRecommended && (
|
|
||||||
<Badge variant="light" color="blue" size="sm">
|
|
||||||
Recommended
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Badge variant="light" color="gray" size="sm">
|
|
||||||
{size}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{isSelected && (
|
|
||||||
<Stack gap="sm" pt="sm">
|
|
||||||
<Button
|
|
||||||
onClick={onDownload}
|
|
||||||
disabled={isDownloading}
|
|
||||||
leftSection={
|
|
||||||
isDownloading ? <Loader size="1rem" /> : <Download size="1rem" />
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
radius="md"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{isDownloading ? 'Downloading...' : 'Download'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isDownloading && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Progress value={downloadProgress} color="blue" radius="xl" />
|
|
||||||
<Text size="xs" c="dimmed" ta="center">
|
|
||||||
{downloadProgress.toFixed(1)}% complete
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
|
|
@ -8,11 +8,24 @@ import { AppearanceTab } from './settings/AppearanceTab';
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
currentScreen?: 'download' | 'launch' | 'interface';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
export const SettingsModal = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
currentScreen,
|
||||||
|
}: SettingsModalProps) => {
|
||||||
const [activeTab, setActiveTab] = useState('general');
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
|
|
||||||
|
const showVersionsTab = currentScreen !== 'download';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showVersionsTab && activeTab === 'versions') {
|
||||||
|
setActiveTab('general');
|
||||||
|
}
|
||||||
|
}, [showVersionsTab, activeTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened) {
|
if (opened) {
|
||||||
const originalOverflow = document.body.style.overflow;
|
const originalOverflow = document.body.style.overflow;
|
||||||
|
|
@ -39,7 +52,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
<Text fw={500}>Settings</Text>
|
<Text fw={500}>Settings</Text>
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
size="lg"
|
size="xl"
|
||||||
centered
|
centered
|
||||||
lockScroll={false}
|
lockScroll={false}
|
||||||
styles={{
|
styles={{
|
||||||
|
|
@ -81,14 +94,16 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
>
|
>
|
||||||
General
|
General
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
{showVersionsTab && (
|
||||||
value="versions"
|
<Tabs.Tab
|
||||||
leftSection={
|
value="versions"
|
||||||
<GitBranch style={{ width: rem(16), height: rem(16) }} />
|
leftSection={
|
||||||
}
|
<GitBranch style={{ width: rem(16), height: rem(16) }} />
|
||||||
>
|
}
|
||||||
Versions
|
>
|
||||||
</Tabs.Tab>
|
Versions
|
||||||
|
</Tabs.Tab>
|
||||||
|
)}
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="appearance"
|
value="appearance"
|
||||||
leftSection={
|
leftSection={
|
||||||
|
|
@ -103,9 +118,11 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
|
||||||
<GeneralTab />
|
<GeneralTab />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="versions">
|
{showVersionsTab && (
|
||||||
<VersionsTab />
|
<Tabs.Panel value="versions">
|
||||||
</Tabs.Panel>
|
<VersionsTab />
|
||||||
|
</Tabs.Panel>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tabs.Panel value="appearance">
|
<Tabs.Panel value="appearance">
|
||||||
<AppearanceTab />
|
<AppearanceTab />
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,14 @@ interface AdvancedTabProps {
|
||||||
serverOnly: boolean;
|
serverOnly: boolean;
|
||||||
noshift: boolean;
|
noshift: boolean;
|
||||||
flashattention: boolean;
|
flashattention: boolean;
|
||||||
|
noavx2: boolean;
|
||||||
|
failsafe: boolean;
|
||||||
onAdditionalArgumentsChange: (args: string) => void;
|
onAdditionalArgumentsChange: (args: string) => void;
|
||||||
onServerOnlyChange: (serverOnly: boolean) => void;
|
onServerOnlyChange: (serverOnly: boolean) => void;
|
||||||
onNoshiftChange: (noshift: boolean) => void;
|
onNoshiftChange: (noshift: boolean) => void;
|
||||||
onFlashattentionChange: (flashattention: boolean) => void;
|
onFlashattentionChange: (flashattention: boolean) => void;
|
||||||
|
onNoavx2Change: (noavx2: boolean) => void;
|
||||||
|
onFailsafeChange: (failsafe: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdvancedTab = ({
|
export const AdvancedTab = ({
|
||||||
|
|
@ -17,10 +21,14 @@ export const AdvancedTab = ({
|
||||||
serverOnly,
|
serverOnly,
|
||||||
noshift,
|
noshift,
|
||||||
flashattention,
|
flashattention,
|
||||||
|
noavx2,
|
||||||
|
failsafe,
|
||||||
onAdditionalArgumentsChange,
|
onAdditionalArgumentsChange,
|
||||||
onServerOnlyChange,
|
onServerOnlyChange,
|
||||||
onNoshiftChange,
|
onNoshiftChange,
|
||||||
onFlashattentionChange,
|
onFlashattentionChange,
|
||||||
|
onNoavx2Change,
|
||||||
|
onFailsafeChange,
|
||||||
}: AdvancedTabProps) => (
|
}: AdvancedTabProps) => (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -40,38 +48,72 @@ export const AdvancedTab = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Group gap="xs" align="center">
|
<Stack gap="md">
|
||||||
<Checkbox
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
checked={serverOnly}
|
<div style={{ minWidth: '200px' }}>
|
||||||
onChange={(event) => onServerOnlyChange(event.currentTarget.checked)}
|
<Group gap="xs" align="center">
|
||||||
label="Server-only mode"
|
<Checkbox
|
||||||
/>
|
checked={serverOnly}
|
||||||
<InfoTooltip label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend." />
|
onChange={(event) =>
|
||||||
</Group>
|
onServerOnlyChange(event.currentTarget.checked)
|
||||||
</div>
|
}
|
||||||
|
label="Server-only mode"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend." />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div style={{ minWidth: '200px' }}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!noshift}
|
checked={!noshift}
|
||||||
onChange={(event) => onNoshiftChange(!event.currentTarget.checked)}
|
onChange={(event) =>
|
||||||
label="Use ContextShift"
|
onNoshiftChange(!event.currentTarget.checked)
|
||||||
/>
|
}
|
||||||
<InfoTooltip label="Use Context Shifting to reduce reprocessing. Recommended" />
|
label="Use ContextShift"
|
||||||
</Group>
|
/>
|
||||||
</div>
|
<InfoTooltip label="Use Context Shifting to reduce reprocessing. Recommended" />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<div>
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<Group gap="xs" align="center">
|
<div style={{ minWidth: '200px' }}>
|
||||||
<Checkbox
|
<Group gap="xs" align="center">
|
||||||
checked={flashattention}
|
<Checkbox
|
||||||
onChange={(event) =>
|
checked={flashattention}
|
||||||
onFlashattentionChange(event.currentTarget.checked)
|
onChange={(event) =>
|
||||||
}
|
onFlashattentionChange(event.currentTarget.checked)
|
||||||
label="Use FlashAttention"
|
}
|
||||||
/>
|
label="Use FlashAttention"
|
||||||
<InfoTooltip label="Enable flash attention for GGUF models." />
|
/>
|
||||||
</Group>
|
<InfoTooltip label="Enable flash attention for GGUF models." />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ minWidth: '200px' }}>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={noavx2}
|
||||||
|
onChange={(event) =>
|
||||||
|
onNoavx2Change(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
label="Disable AVX2"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="Do not use AVX2 instructions, a slower compatibility mode for older devices." />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={failsafe}
|
||||||
|
onChange={(event) => onFailsafeChange(event.currentTarget.checked)}
|
||||||
|
label="Failsafe"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="Use failsafe mode, extremely slow CPU only compatibility mode that should work on all devices. Can be combined with useclblast if your device supports OpenCL." />
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,13 @@ import {
|
||||||
Select,
|
Select,
|
||||||
Badge,
|
Badge,
|
||||||
Card,
|
Card,
|
||||||
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { File, Search } from 'lucide-react';
|
import { File, Search } from 'lucide-react';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { getInputValidationState } from '@/utils/validation';
|
import { getInputValidationState } from '@/utils/validation';
|
||||||
import {
|
import { isNoCudaBinary, isRocmBinary } from '@/utils/binaryUtils';
|
||||||
isOldPcBinary,
|
|
||||||
isNoCudaBinary,
|
|
||||||
isRocmBinary,
|
|
||||||
} from '@/utils/binaryUtils';
|
|
||||||
|
|
||||||
interface GeneralTabProps {
|
interface GeneralTabProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
|
|
@ -47,6 +44,9 @@ export const GeneralTab = ({
|
||||||
onContextSizeChange,
|
onContextSizeChange,
|
||||||
onBackendChange,
|
onBackendChange,
|
||||||
}: GeneralTabProps) => {
|
}: GeneralTabProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const isDark = colorScheme === 'dark';
|
||||||
|
|
||||||
const [availableBackends, setAvailableBackends] = useState<
|
const [availableBackends, setAvailableBackends] = useState<
|
||||||
Array<{ value: string; label: string; devices?: string[] }>
|
Array<{ value: string; label: string; devices?: string[] }>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
@ -57,9 +57,9 @@ export const GeneralTab = ({
|
||||||
setIsLoadingBackends(true);
|
setIsLoadingBackends(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [currentVersion, cpuCapabilities, gpuCapabilities] =
|
const [currentBinaryInfo, _cpuCapabilities, gpuCapabilities] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
window.electronAPI.kobold.getCurrentVersion(),
|
window.electronAPI.kobold.getCurrentBinaryInfo(),
|
||||||
window.electronAPI.kobold.detectCPU(),
|
window.electronAPI.kobold.detectCPU(),
|
||||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||||
]);
|
]);
|
||||||
|
|
@ -70,12 +70,10 @@ export const GeneralTab = ({
|
||||||
devices?: string[];
|
devices?: string[];
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (currentVersion?.filename) {
|
if (currentBinaryInfo?.filename) {
|
||||||
const filename = currentVersion.filename;
|
const filename = currentBinaryInfo.filename;
|
||||||
|
|
||||||
if (!isOldPcBinary(filename) && cpuCapabilities.avx2) {
|
backends.push({ value: 'cpu', label: 'CPU' });
|
||||||
backends.push({ value: 'cpu', label: 'CPU' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) {
|
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) {
|
||||||
backends.push({
|
backends.push({
|
||||||
|
|
@ -110,8 +108,6 @@ export const GeneralTab = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backends.push({ value: 'failsafe', label: 'Failsafe' });
|
|
||||||
|
|
||||||
setAvailableBackends(backends);
|
setAvailableBackends(backends);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -231,8 +227,8 @@ export const GeneralTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
{backend === 'cpu' && (
|
{backend === 'cpu' && (
|
||||||
<Card withBorder p="sm" mt="xs" bg="blue.0">
|
<Card withBorder p="sm" mt="xs" bg={isDark ? 'blue.9' : 'blue.0'}>
|
||||||
<Text size="sm" c="blue.8">
|
<Text size="sm" c={isDark ? 'blue.2' : 'blue.8'}>
|
||||||
<Text component="span" fw={600}>
|
<Text component="span" fw={600}>
|
||||||
Performance Note:
|
Performance Note:
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export const NetworkTab = ({
|
||||||
<div>
|
<div>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<div style={{ minWidth: '250px' }}>
|
<div style={{ minWidth: '200px' }}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={multiuser}
|
checked={multiuser}
|
||||||
|
|
@ -88,7 +88,7 @@ export const NetworkTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ minWidth: '250px' }}>
|
<div style={{ minWidth: '200px' }}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={multiplayer}
|
checked={multiplayer}
|
||||||
|
|
@ -103,7 +103,7 @@ export const NetworkTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<div style={{ minWidth: '250px' }}>
|
<div style={{ minWidth: '200px' }}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={remotetunnel}
|
checked={remotetunnel}
|
||||||
|
|
@ -116,7 +116,7 @@ export const NetworkTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ minWidth: '250px' }}>
|
<div style={{ minWidth: '200px' }}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={nocertify}
|
checked={nocertify}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,16 @@ import {
|
||||||
Badge,
|
Badge,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { DownloadOptionCard } from '@/components/DownloadOptionCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import {
|
import {
|
||||||
getPlatformDisplayName,
|
getPlatformDisplayName,
|
||||||
filterAssetsByPlatform,
|
filterAssetsByPlatform,
|
||||||
} from '@/utils/platform';
|
} from '@/utils/platform';
|
||||||
import { formatFileSize } from '@/utils/fileSize';
|
import { formatFileSize } from '@/utils/fileSize';
|
||||||
import {
|
import {
|
||||||
getAssetDescription,
|
|
||||||
isAssetRecommended,
|
isAssetRecommended,
|
||||||
sortAssetsByRecommendation,
|
sortAssetsByRecommendation,
|
||||||
|
getAssetDescription,
|
||||||
} from '@/utils/assets';
|
} from '@/utils/assets';
|
||||||
import { ROCM } from '@/constants';
|
import { ROCM } from '@/constants';
|
||||||
import type { GitHubAsset, GitHubRelease } from '@/types';
|
import type { GitHubAsset, GitHubRelease } from '@/types';
|
||||||
|
|
@ -32,8 +32,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
|
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
|
||||||
const [selectedAsset, setSelectedAsset] = useState<GitHubAsset | null>(null);
|
|
||||||
const [selectedROCm, setSelectedROCm] = useState<boolean>(false);
|
|
||||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
const [userPlatform, setUserPlatform] = useState<string>('');
|
||||||
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
|
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
|
||||||
const [hasROCm, setHasROCm] = useState<boolean>(false);
|
const [hasROCm, setHasROCm] = useState<boolean>(false);
|
||||||
|
|
@ -43,6 +41,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
const [downloadingType, setDownloadingType] = useState<
|
const [downloadingType, setDownloadingType] = useState<
|
||||||
'asset' | 'rocm' | null
|
'asset' | 'rocm' | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
|
||||||
const [rocmDownload, setRocmDownload] = useState<{
|
const [rocmDownload, setRocmDownload] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -112,54 +111,51 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDownload = async (type: 'asset' | 'rocm' = 'asset') => {
|
const handleDownload = useCallback(
|
||||||
if (type === 'asset' && !selectedAsset) return;
|
async (type: 'asset' | 'rocm', asset?: GitHubAsset) => {
|
||||||
|
if (type === 'asset' && !asset) return;
|
||||||
|
|
||||||
try {
|
|
||||||
setDownloading(true);
|
setDownloading(true);
|
||||||
setDownloadProgress(0);
|
|
||||||
setDownloadingType(type);
|
setDownloadingType(type);
|
||||||
|
setDownloadingAsset(type === 'asset' ? asset!.name : null);
|
||||||
const result =
|
|
||||||
type === 'rocm'
|
|
||||||
? await window.electronAPI.kobold.downloadROCm()
|
|
||||||
: await window.electronAPI.kobold.downloadRelease(selectedAsset!);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
onDownloadComplete();
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
'Download Failed',
|
|
||||||
result.error || `${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`${type === 'rocm' ? 'ROCm' : ''} Download error:`, err);
|
|
||||||
} finally {
|
|
||||||
setDownloading(false);
|
|
||||||
setDownloadProgress(0);
|
setDownloadProgress(0);
|
||||||
setDownloadingType(null);
|
|
||||||
}
|
try {
|
||||||
};
|
await (type === 'rocm'
|
||||||
|
? window.electronAPI.kobold.downloadROCm()
|
||||||
|
: await window.electronAPI.kobold.downloadRelease(asset!));
|
||||||
|
|
||||||
|
onDownloadComplete();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setDownloading(false);
|
||||||
|
setDownloadingType(null);
|
||||||
|
setDownloadingAsset(null);
|
||||||
|
setDownloadProgress(0);
|
||||||
|
}, 200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to download ${type}:`, error);
|
||||||
|
setDownloading(false);
|
||||||
|
setDownloadingType(null);
|
||||||
|
setDownloadingAsset(null);
|
||||||
|
setDownloadProgress(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onDownloadComplete]
|
||||||
|
);
|
||||||
|
|
||||||
const renderROCmCard = () => {
|
const renderROCmCard = () => {
|
||||||
if (!rocmDownload) return null;
|
if (!rocmDownload) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DownloadOptionCard
|
<DownloadCard
|
||||||
name={ROCM.BINARY_NAME}
|
name={ROCM.BINARY_NAME}
|
||||||
description={getAssetDescription(ROCM.BINARY_NAME)}
|
|
||||||
size={formatFileSize(ROCM.SIZE_BYTES)}
|
size={formatFileSize(ROCM.SIZE_BYTES)}
|
||||||
isSelected={selectedROCm}
|
description={getAssetDescription(ROCM.BINARY_NAME)}
|
||||||
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
|
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
|
||||||
isDownloading={downloading && downloadingType === 'rocm'}
|
isDownloading={downloading && downloadingType === 'rocm'}
|
||||||
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
|
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
|
||||||
onClick={() => {
|
disabled={downloading && downloadingType !== 'rocm'}
|
||||||
if (!selectedROCm) {
|
|
||||||
setSelectedROCm(true);
|
|
||||||
setSelectedAsset(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload('rocm');
|
handleDownload('rocm');
|
||||||
|
|
@ -178,7 +174,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Stack align="center" gap="md" py="xl">
|
<Stack align="center" gap="md" py="xl">
|
||||||
<Loader color="blue" />
|
<Loader color="blue" />
|
||||||
<Text c="dimmed">Loading latest release...</Text>
|
<Text c="dimmed">Preparing download options...</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -253,12 +249,11 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
filteredAssets,
|
filteredAssets,
|
||||||
hasAMDGPU
|
hasAMDGPU
|
||||||
).map((asset) => (
|
).map((asset) => (
|
||||||
<DownloadOptionCard
|
<DownloadCard
|
||||||
key={asset.name}
|
key={asset.name}
|
||||||
name={asset.name}
|
name={asset.name}
|
||||||
description={getAssetDescription(asset.name)}
|
|
||||||
size={formatFileSize(asset.size)}
|
size={formatFileSize(asset.size)}
|
||||||
isSelected={selectedAsset?.name === asset.name}
|
description={getAssetDescription(asset.name)}
|
||||||
isRecommended={isAssetRecommended(
|
isRecommended={isAssetRecommended(
|
||||||
asset.name,
|
asset.name,
|
||||||
hasAMDGPU
|
hasAMDGPU
|
||||||
|
|
@ -266,24 +261,21 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
isDownloading={
|
isDownloading={
|
||||||
downloading &&
|
downloading &&
|
||||||
downloadingType === 'asset' &&
|
downloadingType === 'asset' &&
|
||||||
selectedAsset?.name === asset.name
|
downloadingAsset === asset.name
|
||||||
}
|
}
|
||||||
downloadProgress={
|
downloadProgress={
|
||||||
downloading &&
|
downloading &&
|
||||||
downloadingType === 'asset' &&
|
downloadingType === 'asset' &&
|
||||||
selectedAsset?.name === asset.name
|
downloadingAsset === asset.name
|
||||||
? downloadProgress
|
? downloadProgress
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
onClick={() => {
|
disabled={
|
||||||
if (selectedAsset?.name !== asset.name) {
|
downloading && downloadingAsset !== asset.name
|
||||||
setSelectedAsset(asset);
|
}
|
||||||
setSelectedROCm(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload();
|
handleDownload('asset', asset);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
websearch,
|
websearch,
|
||||||
noshift,
|
noshift,
|
||||||
flashattention,
|
flashattention,
|
||||||
|
noavx2,
|
||||||
|
failsafe,
|
||||||
backend,
|
backend,
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
|
|
@ -66,6 +68,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
handleWebsearchChange,
|
handleWebsearchChange,
|
||||||
handleNoshiftChange,
|
handleNoshiftChange,
|
||||||
handleFlashattentionChange,
|
handleFlashattentionChange,
|
||||||
|
handleNoavx2Change,
|
||||||
|
handleFailsafeChange,
|
||||||
handleBackendChange,
|
handleBackendChange,
|
||||||
} = useLaunchConfig();
|
} = useLaunchConfig();
|
||||||
|
|
||||||
|
|
@ -155,6 +159,16 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNoavx2ChangeWithTracking = (noavx2: boolean) => {
|
||||||
|
handleNoavx2Change(noavx2);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFailsafeChangeWithTracking = (failsafe: boolean) => {
|
||||||
|
handleFailsafeChange(failsafe);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
|
const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
|
||||||
handleMultiuserChange(multiuser);
|
handleMultiuserChange(multiuser);
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
|
|
@ -199,7 +213,6 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [loadConfigFiles]);
|
}, [loadConfigFiles]);
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const handleLaunch = async () => {
|
const handleLaunch = async () => {
|
||||||
if (isLaunching || !modelPath) {
|
if (isLaunching || !modelPath) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -271,8 +284,6 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
args.push('--usevulkan');
|
args.push('--usevulkan');
|
||||||
} else if (backend === 'clblast') {
|
} else if (backend === 'clblast') {
|
||||||
args.push('--useclblast');
|
args.push('--useclblast');
|
||||||
} else if (backend === 'failsafe') {
|
|
||||||
args.push('--failsafe');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,6 +386,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
serverOnly={serverOnly}
|
serverOnly={serverOnly}
|
||||||
noshift={noshift}
|
noshift={noshift}
|
||||||
flashattention={flashattention}
|
flashattention={flashattention}
|
||||||
|
noavx2={noavx2}
|
||||||
|
failsafe={failsafe}
|
||||||
onAdditionalArgumentsChange={
|
onAdditionalArgumentsChange={
|
||||||
handleAdditionalArgumentsChangeWithTracking
|
handleAdditionalArgumentsChangeWithTracking
|
||||||
}
|
}
|
||||||
|
|
@ -383,6 +396,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
onFlashattentionChange={
|
onFlashattentionChange={
|
||||||
handleFlashattentionChangeWithTracking
|
handleFlashattentionChangeWithTracking
|
||||||
}
|
}
|
||||||
|
onNoavx2Change={handleNoavx2ChangeWithTracking}
|
||||||
|
onFailsafeChange={handleFailsafeChangeWithTracking}
|
||||||
/>
|
/>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import {
|
||||||
Group,
|
Group,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Badge,
|
|
||||||
Progress,
|
|
||||||
Loader,
|
Loader,
|
||||||
rem,
|
rem,
|
||||||
Center,
|
Center,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { RotateCcw, Download } from 'lucide-react';
|
import { RotateCcw } from 'lucide-react';
|
||||||
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import { isAssetCompatibleWithPlatform } from '@/utils/platform';
|
import { isAssetCompatibleWithPlatform } from '@/utils/platform';
|
||||||
|
import { getAssetDescription } from '@/utils/assets';
|
||||||
import type {
|
import type {
|
||||||
InstalledVersion,
|
InstalledVersion,
|
||||||
GitHubAsset,
|
GitHubAsset,
|
||||||
|
|
@ -27,6 +27,7 @@ interface VersionInfo {
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
downloadUrl?: string;
|
downloadUrl?: string;
|
||||||
installedPath?: string;
|
installedPath?: string;
|
||||||
|
isROCm?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VersionsTab = () => {
|
export const VersionsTab = () => {
|
||||||
|
|
@ -37,6 +38,12 @@ export const VersionsTab = () => {
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [availableAssets, setAvailableAssets] = useState<GitHubAsset[]>([]);
|
const [availableAssets, setAvailableAssets] = useState<GitHubAsset[]>([]);
|
||||||
|
const [rocmDownload, setRocmDownload] = useState<{
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
size: number;
|
||||||
|
version?: string;
|
||||||
|
} | null>(null);
|
||||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
@ -60,14 +67,28 @@ export const VersionsTab = () => {
|
||||||
|
|
||||||
setInstalledVersions(versions);
|
setInstalledVersions(versions);
|
||||||
|
|
||||||
if (currentBinaryPath) {
|
if (currentBinaryPath && versions.length > 0) {
|
||||||
const current = versions.find(
|
const current = versions.find((v) => v.path === currentBinaryPath);
|
||||||
(v) =>
|
if (current) {
|
||||||
v.path === currentBinaryPath || v.filename === currentBinaryPath
|
setCurrentVersion(current);
|
||||||
|
} else {
|
||||||
|
setCurrentVersion(versions[0]);
|
||||||
|
await window.electronAPI.config.set(
|
||||||
|
'currentKoboldBinary',
|
||||||
|
versions[0].path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (versions.length > 0) {
|
||||||
|
setCurrentVersion(versions[0]);
|
||||||
|
await window.electronAPI.config.set(
|
||||||
|
'currentKoboldBinary',
|
||||||
|
versions[0].path
|
||||||
);
|
);
|
||||||
setCurrentVersion(current || null);
|
|
||||||
} else {
|
} else {
|
||||||
setCurrentVersion(versions[0] || null);
|
setCurrentVersion(null);
|
||||||
|
if (currentBinaryPath) {
|
||||||
|
await window.electronAPI.config.set('currentKoboldBinary', '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load installed versions:', error);
|
console.error('Failed to load installed versions:', error);
|
||||||
|
|
@ -81,7 +102,10 @@ export const VersionsTab = () => {
|
||||||
setLoadingRemote(true);
|
setLoadingRemote(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const release = await window.electronAPI.kobold.getLatestRelease();
|
const [release, rocm] = await Promise.all([
|
||||||
|
window.electronAPI.kobold.getLatestRelease(),
|
||||||
|
window.electronAPI.kobold.getROCmDownload(),
|
||||||
|
]);
|
||||||
|
|
||||||
if (release) {
|
if (release) {
|
||||||
setLatestRelease(release);
|
setLatestRelease(release);
|
||||||
|
|
@ -90,6 +114,8 @@ export const VersionsTab = () => {
|
||||||
);
|
);
|
||||||
setAvailableAssets(compatibleAssets);
|
setAvailableAssets(compatibleAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRocmDownload(rocm);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load remote versions:', error);
|
console.error('Failed to load remote versions:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -134,13 +160,32 @@ export const VersionsTab = () => {
|
||||||
};
|
};
|
||||||
}, [downloading]);
|
}, [downloading]);
|
||||||
|
|
||||||
|
const getDisplayNameFromPath = (
|
||||||
|
installedVersion: InstalledVersion
|
||||||
|
): string => {
|
||||||
|
const pathParts = installedVersion.path.split(/[/\\]/);
|
||||||
|
const launcherIndex = pathParts.findIndex(
|
||||||
|
(part) =>
|
||||||
|
part === 'koboldcpp-launcher' ||
|
||||||
|
part === 'koboldcpp.exe' ||
|
||||||
|
part === 'koboldcpp'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launcherIndex > 0) {
|
||||||
|
return pathParts[launcherIndex - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return installedVersion.filename;
|
||||||
|
};
|
||||||
|
|
||||||
const getAllVersions = (): VersionInfo[] => {
|
const getAllVersions = (): VersionInfo[] => {
|
||||||
const versions: VersionInfo[] = [];
|
const versions: VersionInfo[] = [];
|
||||||
|
|
||||||
availableAssets.forEach((asset) => {
|
availableAssets.forEach((asset) => {
|
||||||
const installedVersion = installedVersions.find(
|
const installedVersion = installedVersions.find((v) => {
|
||||||
(v) => v.filename === asset.name
|
const displayName = getDisplayNameFromPath(v);
|
||||||
);
|
return displayName === asset.name;
|
||||||
|
});
|
||||||
|
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(
|
||||||
installedVersion &&
|
installedVersion &&
|
||||||
|
|
@ -154,31 +199,63 @@ export const VersionsTab = () => {
|
||||||
installedVersion?.version ||
|
installedVersion?.version ||
|
||||||
latestRelease?.tag_name.replace(/^v/, '') ||
|
latestRelease?.tag_name.replace(/^v/, '') ||
|
||||||
'unknown',
|
'unknown',
|
||||||
size: installedVersion?.size || asset.size,
|
size: installedVersion ? undefined : asset.size,
|
||||||
isInstalled: Boolean(installedVersion),
|
isInstalled: Boolean(installedVersion),
|
||||||
isCurrent,
|
isCurrent,
|
||||||
downloadUrl: asset.browser_download_url,
|
downloadUrl: asset.browser_download_url,
|
||||||
installedPath: installedVersion?.path,
|
installedPath: installedVersion?.path,
|
||||||
|
isROCm: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
installedVersions.forEach((installed) => {
|
if (rocmDownload) {
|
||||||
const existsInRemote = versions.some(
|
const installedVersion = installedVersions.find((v) => {
|
||||||
(v) => v.name === installed.filename
|
const displayName = getDisplayNameFromPath(v);
|
||||||
|
return displayName === rocmDownload.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCurrent = Boolean(
|
||||||
|
installedVersion &&
|
||||||
|
currentVersion &&
|
||||||
|
currentVersion.path === installedVersion.path
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const existsInVersions = versions.some(
|
||||||
|
(v) => v.name === rocmDownload.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existsInVersions) {
|
||||||
|
versions.push({
|
||||||
|
name: rocmDownload.name,
|
||||||
|
version:
|
||||||
|
installedVersion?.version || rocmDownload.version || 'unknown',
|
||||||
|
size: installedVersion ? undefined : rocmDownload.size,
|
||||||
|
isInstalled: Boolean(installedVersion),
|
||||||
|
isCurrent,
|
||||||
|
downloadUrl: rocmDownload.url,
|
||||||
|
installedPath: installedVersion?.path,
|
||||||
|
isROCm: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installedVersions.forEach((installed) => {
|
||||||
|
const displayName = getDisplayNameFromPath(installed);
|
||||||
|
const existsInRemote = versions.some((v) => v.name === displayName);
|
||||||
|
|
||||||
if (!existsInRemote) {
|
if (!existsInRemote) {
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(
|
||||||
currentVersion && currentVersion.path === installed.path
|
currentVersion && currentVersion.path === installed.path
|
||||||
);
|
);
|
||||||
|
|
||||||
versions.push({
|
versions.push({
|
||||||
name: installed.filename,
|
name: displayName,
|
||||||
version: installed.version,
|
version: installed.version,
|
||||||
size: installed.size,
|
size: undefined,
|
||||||
isInstalled: true,
|
isInstalled: true,
|
||||||
isCurrent,
|
isCurrent,
|
||||||
installedPath: installed.path,
|
installedPath: installed.path,
|
||||||
|
isROCm: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -186,12 +263,22 @@ export const VersionsTab = () => {
|
||||||
return versions;
|
return versions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (asset: GitHubAsset) => {
|
const handleDownload = async (version: VersionInfo) => {
|
||||||
setDownloading(asset.name);
|
setDownloading(version.name);
|
||||||
setDownloadProgress((prev) => ({ ...prev, [asset.name]: 0 }));
|
setDownloadProgress((prev) => ({ ...prev, [version.name]: 0 }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.kobold.downloadRelease(asset);
|
let result;
|
||||||
|
|
||||||
|
if (version.isROCm) {
|
||||||
|
result = await window.electronAPI.kobold.downloadROCm();
|
||||||
|
} else {
|
||||||
|
const asset = availableAssets.find((a) => a.name === version.name);
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error('Asset not found');
|
||||||
|
}
|
||||||
|
result = await window.electronAPI.kobold.downloadRelease(asset);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadInstalledVersions();
|
await loadInstalledVersions();
|
||||||
|
|
@ -202,7 +289,7 @@ export const VersionsTab = () => {
|
||||||
setDownloading(null);
|
setDownloading(null);
|
||||||
setDownloadProgress((prev) => {
|
setDownloadProgress((prev) => {
|
||||||
const newProgress = { ...prev };
|
const newProgress = { ...prev };
|
||||||
delete newProgress[asset.name];
|
delete newProgress[version.name];
|
||||||
return newProgress;
|
return newProgress;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -275,92 +362,37 @@ export const VersionsTab = () => {
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{getAllVersions().map((version, index) => {
|
{getAllVersions().map((version, index) => {
|
||||||
const isDownloading = downloading === version.name;
|
const isDownloading = downloading === version.name;
|
||||||
const asset = availableAssets.find((a) => a.name === version.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<DownloadCard
|
||||||
key={`${version.name}-${version.version}-${index}`}
|
key={`${version.name}-${version.version}-${index}`}
|
||||||
withBorder
|
name={version.name}
|
||||||
radius="sm"
|
size={
|
||||||
padding="sm"
|
version.size
|
||||||
bd={
|
? `${(version.size / 1024 / 1024).toFixed(1)} MB`
|
||||||
version.isCurrent
|
: ''
|
||||||
? '2px solid var(--mantine-color-blue-filled)'
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
bg={
|
version={version.version}
|
||||||
version.isCurrent
|
description={getAssetDescription(version.name)}
|
||||||
? 'var(--mantine-color-blue-light)'
|
isCurrent={version.isCurrent}
|
||||||
: undefined
|
isInstalled={version.isInstalled}
|
||||||
}
|
isDownloading={isDownloading}
|
||||||
>
|
downloadProgress={downloadProgress[version.name]}
|
||||||
<Group justify="space-between" align="center">
|
disabled={downloading !== null}
|
||||||
<div style={{ flex: 1 }}>
|
onDownload={
|
||||||
<Group gap="xs" align="center" mb="xs">
|
!version.isInstalled
|
||||||
<Text fw={500} size="sm">
|
? (e) => {
|
||||||
{version.name}
|
|
||||||
</Text>
|
|
||||||
{version.isCurrent && (
|
|
||||||
<Badge variant="light" color="blue" size="sm">
|
|
||||||
Current
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
Version {version.version}
|
|
||||||
{version.size && (
|
|
||||||
<> • {(version.size / 1024 / 1024).toFixed(1)} MB</>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!version.isInstalled && asset && (
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
size="xs"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload(asset);
|
handleDownload(version);
|
||||||
}}
|
|
||||||
loading={isDownloading}
|
|
||||||
disabled={downloading !== null}
|
|
||||||
leftSection={
|
|
||||||
isDownloading ? (
|
|
||||||
<Loader size="1rem" />
|
|
||||||
) : (
|
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
: undefined
|
||||||
{isDownloading ? 'Downloading...' : 'Download'}
|
}
|
||||||
</Button>
|
onMakeCurrent={
|
||||||
)}
|
version.isInstalled && !version.isCurrent
|
||||||
|
? () => makeCurrent(version)
|
||||||
{version.isInstalled && !version.isCurrent && (
|
: undefined
|
||||||
<Button
|
}
|
||||||
variant="light"
|
/>
|
||||||
size="xs"
|
|
||||||
onClick={() => makeCurrent(version)}
|
|
||||||
>
|
|
||||||
Make Current
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{isDownloading &&
|
|
||||||
downloadProgress[version.name] !== undefined && (
|
|
||||||
<Stack gap="xs" mt="sm">
|
|
||||||
<Progress
|
|
||||||
value={downloadProgress[version.name]}
|
|
||||||
color="blue"
|
|
||||||
radius="xl"
|
|
||||||
/>
|
|
||||||
<Text size="xs" c="dimmed" ta="center">
|
|
||||||
{downloadProgress[version.name].toFixed(1)}% complete
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ export const useLaunchConfig = () => {
|
||||||
const [websearch, setWebsearch] = useState<boolean>(false);
|
const [websearch, setWebsearch] = useState<boolean>(false);
|
||||||
const [noshift, setNoshift] = useState<boolean>(false);
|
const [noshift, setNoshift] = useState<boolean>(false);
|
||||||
const [flashattention, setFlashattention] = useState<boolean>(false);
|
const [flashattention, setFlashattention] = useState<boolean>(false);
|
||||||
const [backend, setBackend] = useState<string>('');
|
const [noavx2, setNoavx2] = useState<boolean>(false);
|
||||||
|
const [failsafe, setFailsafe] = useState<boolean>(false);
|
||||||
|
const [backend, setBackend] = useState<string>('cpu');
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||||
|
|
@ -94,10 +96,33 @@ export const useLaunchConfig = () => {
|
||||||
setFlashattention(false);
|
setFlashattention(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof configData.backend === 'string') {
|
if (typeof configData.noavx2 === 'boolean') {
|
||||||
setBackend(configData.backend);
|
setNoavx2(configData.noavx2);
|
||||||
} else {
|
} else {
|
||||||
setBackend('');
|
setNoavx2(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.failsafe === 'boolean') {
|
||||||
|
setFailsafe(configData.failsafe);
|
||||||
|
} else {
|
||||||
|
setFailsafe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configData.usecuda !== null && configData.usecuda !== undefined) {
|
||||||
|
const gpuInfo = await window.electronAPI.kobold.detectGPU();
|
||||||
|
setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm');
|
||||||
|
} else if (
|
||||||
|
configData.usevulkan !== null &&
|
||||||
|
configData.usevulkan !== undefined
|
||||||
|
) {
|
||||||
|
setBackend('vulkan');
|
||||||
|
} else if (
|
||||||
|
configData.useclblast !== null &&
|
||||||
|
configData.useclblast !== undefined
|
||||||
|
) {
|
||||||
|
setBackend('clblast');
|
||||||
|
} else {
|
||||||
|
setBackend('cpu');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setGpuLayers(0);
|
setGpuLayers(0);
|
||||||
|
|
@ -111,7 +136,9 @@ export const useLaunchConfig = () => {
|
||||||
setWebsearch(false);
|
setWebsearch(false);
|
||||||
setNoshift(false);
|
setNoshift(false);
|
||||||
setFlashattention(false);
|
setFlashattention(false);
|
||||||
setBackend('');
|
setNoavx2(false);
|
||||||
|
setFailsafe(false);
|
||||||
|
setBackend('cpu');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -131,7 +158,9 @@ export const useLaunchConfig = () => {
|
||||||
setWebsearch(false);
|
setWebsearch(false);
|
||||||
setNoshift(false);
|
setNoshift(false);
|
||||||
setFlashattention(false);
|
setFlashattention(false);
|
||||||
setBackend('');
|
setNoavx2(false);
|
||||||
|
setFailsafe(false);
|
||||||
|
setBackend('cpu');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadConfigFromFile = useCallback(
|
const loadConfigFromFile = useCallback(
|
||||||
|
|
@ -237,6 +266,14 @@ export const useLaunchConfig = () => {
|
||||||
setFlashattention(checked);
|
setFlashattention(checked);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleNoavx2Change = useCallback((checked: boolean) => {
|
||||||
|
setNoavx2(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFailsafeChange = useCallback((checked: boolean) => {
|
||||||
|
setFailsafe(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleBackendChange = useCallback((backend: string) => {
|
const handleBackendChange = useCallback((backend: string) => {
|
||||||
setBackend(backend);
|
setBackend(backend);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -257,6 +294,8 @@ export const useLaunchConfig = () => {
|
||||||
websearch,
|
websearch,
|
||||||
noshift,
|
noshift,
|
||||||
flashattention,
|
flashattention,
|
||||||
|
noavx2,
|
||||||
|
failsafe,
|
||||||
backend,
|
backend,
|
||||||
|
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
|
|
@ -278,6 +317,8 @@ export const useLaunchConfig = () => {
|
||||||
handleWebsearchChange,
|
handleWebsearchChange,
|
||||||
handleNoshiftChange,
|
handleNoshiftChange,
|
||||||
handleFlashattentionChange,
|
handleFlashattentionChange,
|
||||||
|
handleNoavx2Change,
|
||||||
|
handleFailsafeChange,
|
||||||
handleBackendChange,
|
handleBackendChange,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,29 @@ class FriendlyKoboldApp {
|
||||||
app.on('before-quit', async (event) => {
|
app.on('before-quit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
await this.koboldManager.cleanup();
|
try {
|
||||||
|
const cleanupPromise = this.koboldManager.cleanup();
|
||||||
|
const timeoutPromise = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([cleanupPromise, timeoutPromise]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during KoboldCpp cleanup:', error);
|
||||||
|
}
|
||||||
|
|
||||||
this.windowManager.cleanup();
|
this.windowManager.cleanup();
|
||||||
|
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('will-quit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (!this.windowManager.getMainWindow()) {
|
if (!this.windowManager.getMainWindow()) {
|
||||||
this.windowManager.createMainWindow();
|
this.windowManager.createMainWindow();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
createWriteStream,
|
createWriteStream,
|
||||||
chmodSync,
|
chmodSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
|
unlinkSync,
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
import { GitHubService } from '@/main/services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
|
|
@ -74,7 +75,9 @@ export class KoboldCppManager {
|
||||||
asset: GitHubAsset,
|
asset: GitHubAsset,
|
||||||
onProgress?: (progress: number) => void
|
onProgress?: (progress: number) => void
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const filePath = join(this.installDir, asset.name);
|
const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`);
|
||||||
|
const baseFilename = asset.name.replace(/\.exe$/, '');
|
||||||
|
const unpackedDirPath = join(this.installDir, baseFilename);
|
||||||
|
|
||||||
const response = await fetch(asset.browser_download_url);
|
const response = await fetch(asset.browser_download_url);
|
||||||
|
|
||||||
|
|
@ -90,7 +93,7 @@ export class KoboldCppManager {
|
||||||
throw new Error('Failed to get response reader');
|
throw new Error('Failed to get response reader');
|
||||||
}
|
}
|
||||||
|
|
||||||
const writer = createWriteStream(filePath);
|
const writer = createWriteStream(tempPackedFilePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -119,77 +122,151 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
try {
|
try {
|
||||||
chmodSync(filePath, 0o755);
|
chmodSync(tempPackedFilePath, 0o755);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to make binary executable:', error);
|
console.error('Failed to make binary executable:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
try {
|
||||||
if (!currentBinary) {
|
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
|
||||||
this.configManager.setCurrentKoboldBinary(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePath;
|
try {
|
||||||
|
unlinkSync(tempPackedFilePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to cleanup packed file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const launcherPath = this.getLauncherPath(unpackedDirPath);
|
||||||
|
if (launcherPath && existsSync(launcherPath)) {
|
||||||
|
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||||
|
if (!currentBinary) {
|
||||||
|
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('versions-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return launcherPath;
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to find koboldcpp-launcher after unpacking');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to unpack KoboldCpp:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstalledVersions(
|
private async unpackKoboldCpp(
|
||||||
includeVersions = true
|
packedPath: string,
|
||||||
): Promise<InstalledVersion[]> {
|
unpackDir: string
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const unpackProcess = spawn(packedPath, ['--unpack', unpackDir], {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let errorOutput = '';
|
||||||
|
|
||||||
|
unpackProcess.stdout?.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
unpackProcess.stderr?.on('data', (data) => {
|
||||||
|
errorOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
unpackProcess.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Unpack failed with code ${code}: ${errorOutput || output}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
unpackProcess.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
unpackProcess.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
void 0;
|
||||||
|
}
|
||||||
|
reject(new Error('Unpack process timed out'));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLauncherPath(unpackedDir: string): string | null {
|
||||||
|
const extensions =
|
||||||
|
process.platform === 'win32' ? ['.exe', ''] : ['', '.exe'];
|
||||||
|
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const launcherPath = join(unpackedDir, `koboldcpp-launcher${ext}`);
|
||||||
|
if (existsSync(launcherPath)) {
|
||||||
|
return launcherPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
||||||
try {
|
try {
|
||||||
if (!existsSync(this.installDir)) {
|
if (!existsSync(this.installDir)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = readdirSync(this.installDir);
|
const items = readdirSync(this.installDir);
|
||||||
const koboldFiles = files.filter((file) => {
|
const launchers: Array<{ path: string; filename: string; size: number }> =
|
||||||
const filePath = join(this.installDir, file);
|
[];
|
||||||
try {
|
|
||||||
const stats = statSync(filePath);
|
|
||||||
|
|
||||||
if (stats.isFile() && file.startsWith('koboldcpp')) {
|
for (const item of items) {
|
||||||
if (process.platform !== 'win32') {
|
const itemPath = join(this.installDir, item);
|
||||||
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
|
const stats = statSync(itemPath);
|
||||||
return isExecutable;
|
|
||||||
} else {
|
if (stats.isDirectory()) {
|
||||||
return true;
|
const launcherPath = this.getLauncherPath(itemPath);
|
||||||
}
|
if (launcherPath && existsSync(launcherPath)) {
|
||||||
|
const launcherStats = statSync(launcherPath);
|
||||||
|
const launcherFilename = launcherPath.split(/[/\\]/).pop() || '';
|
||||||
|
launchers.push({
|
||||||
|
path: launcherPath,
|
||||||
|
filename: launcherFilename,
|
||||||
|
size: launcherStats.size,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (!includeVersions) {
|
|
||||||
return koboldFiles.map((file) => {
|
|
||||||
const filePath = join(this.installDir, file);
|
|
||||||
const stats = statSync(filePath);
|
|
||||||
return {
|
|
||||||
version: 'unknown',
|
|
||||||
path: filePath,
|
|
||||||
filename: file,
|
|
||||||
size: stats.size,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionPromises = koboldFiles.map(async (file) => {
|
const versionPromises = launchers.map(async (launcher) => {
|
||||||
const filePath = join(this.installDir, file);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = statSync(filePath);
|
const detectedVersion = await this.getVersionFromBinary(
|
||||||
const detectedVersion = await this.getVersionFromBinary(filePath);
|
launcher.path
|
||||||
|
);
|
||||||
const version = detectedVersion || 'unknown';
|
const version = detectedVersion || 'unknown';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
path: filePath,
|
path: launcher.path,
|
||||||
filename: file,
|
filename: launcher.filename,
|
||||||
size: stats.size,
|
size: launcher.size,
|
||||||
} as InstalledVersion;
|
} as InstalledVersion;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Could not detect version for ${file}:`, error);
|
console.error(
|
||||||
|
`Could not detect version for ${launcher.filename}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -199,7 +276,7 @@ export class KoboldCppManager {
|
||||||
(version): version is InstalledVersion => version !== null
|
(version): version is InstalledVersion => version !== null
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error scanning install directory:', error);
|
console.error('Error scanning install directory:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +307,7 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error scanning for config files:', error);
|
console.error('Error scanning for config files:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
@ -252,7 +329,7 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error parsing config file:', error);
|
console.error('Error parsing config file:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,45 +356,64 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error selecting model file:', error);
|
console.error('Error selecting model file:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentVersion(): Promise<InstalledVersion | null> {
|
async getCurrentVersion(): Promise<InstalledVersion | null> {
|
||||||
const currentBinaryPath = this.configManager.getCurrentKoboldBinary();
|
const currentBinaryPath = this.configManager.getCurrentKoboldBinary();
|
||||||
|
const versions = await this.getInstalledVersions();
|
||||||
|
|
||||||
if (currentBinaryPath && existsSync(currentBinaryPath)) {
|
if (currentBinaryPath && existsSync(currentBinaryPath)) {
|
||||||
try {
|
const currentVersion = versions.find((v) => v.path === currentBinaryPath);
|
||||||
const filename = currentBinaryPath.split(/[/\\]/).pop() || '';
|
if (currentVersion) {
|
||||||
const version =
|
return currentVersion;
|
||||||
(await this.getVersionFromBinary(currentBinaryPath)) || 'unknown';
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
path: currentBinaryPath,
|
|
||||||
filename,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to get current version info:', error);
|
|
||||||
this.configManager.setCurrentKoboldBinary('');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const versions = await this.getInstalledVersions();
|
|
||||||
const firstVersion = versions[0];
|
const firstVersion = versions[0];
|
||||||
|
|
||||||
if (firstVersion) {
|
if (firstVersion) {
|
||||||
this.configManager.setCurrentKoboldBinary(firstVersion.path);
|
this.configManager.setCurrentKoboldBinary(firstVersion.path);
|
||||||
return firstVersion;
|
return firstVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentBinaryPath) {
|
||||||
|
this.configManager.setCurrentKoboldBinary('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentBinaryInfo(): Promise<{
|
||||||
|
path: string;
|
||||||
|
filename: string;
|
||||||
|
} | null> {
|
||||||
|
const currentVersion = await this.getCurrentVersion();
|
||||||
|
|
||||||
|
if (currentVersion) {
|
||||||
|
const pathParts = currentVersion.path.split(/[/\\]/);
|
||||||
|
const filename =
|
||||||
|
pathParts[pathParts.length - 2] || currentVersion.filename;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: currentVersion.path,
|
||||||
|
filename,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setCurrentVersion(binaryPath: string): Promise<boolean> {
|
async setCurrentVersion(binaryPath: string): Promise<boolean> {
|
||||||
if (existsSync(binaryPath)) {
|
if (existsSync(binaryPath)) {
|
||||||
this.configManager.setCurrentKoboldBinary(binaryPath);
|
this.configManager.setCurrentKoboldBinary(binaryPath);
|
||||||
|
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('versions-updated');
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,6 +431,7 @@ export class KoboldCppManager {
|
||||||
const process = spawn(binaryPath, ['--version'], {
|
const process = spawn(binaryPath, ['--version'], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
|
detached: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
|
|
@ -519,7 +616,7 @@ export class KoboldCppManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch Windows ROCm release:', error);
|
console.error('Failed to fetch Windows ROCm release:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,6 +637,13 @@ export class KoboldCppManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tempPackedFilePath = join(
|
||||||
|
this.installDir,
|
||||||
|
`${rocmInfo.name}.packed`
|
||||||
|
);
|
||||||
|
const baseFilename = rocmInfo.name.replace(/\.exe$/, '');
|
||||||
|
const unpackedDirPath = join(this.installDir, baseFilename);
|
||||||
|
|
||||||
const response = await fetch(rocmInfo.url);
|
const response = await fetch(rocmInfo.url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -559,8 +663,7 @@ export class KoboldCppManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = join(this.installDir, rocmInfo.name);
|
const writer = createWriteStream(tempPackedFilePath);
|
||||||
const writer = createWriteStream(filePath);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -588,16 +691,50 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
try {
|
try {
|
||||||
chmodSync(filePath, 0o755);
|
chmodSync(tempPackedFilePath, 0o755);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to make ROCm binary executable:', error);
|
console.error('Failed to make ROCm binary executable:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
try {
|
||||||
success: true,
|
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
|
||||||
path: filePath,
|
|
||||||
};
|
try {
|
||||||
|
unlinkSync(tempPackedFilePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to cleanup packed ROCm file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const launcherPath = this.getLauncherPath(unpackedDirPath);
|
||||||
|
if (launcherPath && existsSync(launcherPath)) {
|
||||||
|
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||||
|
if (!currentBinary) {
|
||||||
|
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('versions-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: launcherPath,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to find koboldcpp-launcher after unpacking ROCm',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to unpack ROCm:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to unpack ROCm: ${(error as Error).message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -663,8 +800,20 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
const availableAssets = latestRelease.assets.map((asset: GitHubAsset) => {
|
const availableAssets = latestRelease.assets.map((asset: GitHubAsset) => {
|
||||||
const installedVersion = installedVersions.find((v) => {
|
const installedVersion = installedVersions.find((v) => {
|
||||||
const filename = v.filename || v.path.split(/[/\\]/).pop() || '';
|
const pathParts = v.path.split(/[/\\]/);
|
||||||
return filename === asset.name;
|
const launcherIndex = pathParts.findIndex(
|
||||||
|
(part) =>
|
||||||
|
part === 'koboldcpp-launcher' ||
|
||||||
|
part === 'koboldcpp.exe' ||
|
||||||
|
part === 'koboldcpp'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launcherIndex > 0) {
|
||||||
|
const directoryName = pathParts[launcherIndex - 1];
|
||||||
|
return directoryName === asset.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -709,6 +858,11 @@ export class KoboldCppManager {
|
||||||
const child = spawn(currentVersion.path, finalArgs, {
|
const child = spawn(currentVersion.path, finalArgs, {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
detached: false,
|
detached: false,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PYTHONDONTWRITEBYTECODE: '1',
|
||||||
|
PYTHONUNBUFFERED: '1',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.koboldProcess = child;
|
this.koboldProcess = child;
|
||||||
|
|
@ -774,14 +928,14 @@ export class KoboldCppManager {
|
||||||
try {
|
try {
|
||||||
this.koboldProcess.kill('SIGKILL');
|
this.koboldProcess.kill('SIGKILL');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error force-killing KoboldCpp process:', error);
|
console.error('Error force-killing KoboldCpp process:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
this.koboldProcess = null;
|
this.koboldProcess = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error stopping KoboldCpp process:', error);
|
console.error('Error stopping KoboldCpp process:', error);
|
||||||
this.koboldProcess = null;
|
this.koboldProcess = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -789,7 +943,7 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
if (this.koboldProcess) {
|
if (this.koboldProcess) {
|
||||||
return new Promise((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
if (!this.koboldProcess) {
|
if (!this.koboldProcess) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
|
|
@ -817,7 +971,7 @@ export class KoboldCppManager {
|
||||||
cleanup();
|
cleanup();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error during cleanup:', error);
|
console.error('Error during cleanup:', error);
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -203,14 +203,22 @@ export class HardwareService {
|
||||||
if (name) {
|
if (name) {
|
||||||
let deviceType = '';
|
let deviceType = '';
|
||||||
|
|
||||||
|
const searchRangeLines = 20;
|
||||||
|
const searchStartIndex = Math.max(0, i - searchRangeLines);
|
||||||
|
const searchEndIndex = Math.min(
|
||||||
|
lines.length,
|
||||||
|
i + searchRangeLines
|
||||||
|
);
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let j = Math.max(0, i - 10);
|
let searchIndex = searchStartIndex;
|
||||||
j < Math.min(lines.length, i + 10);
|
searchIndex < searchEndIndex;
|
||||||
j++
|
searchIndex++
|
||||||
) {
|
) {
|
||||||
if (lines[j].includes('Device Type:')) {
|
if (lines[searchIndex].includes('Device Type:')) {
|
||||||
deviceType =
|
deviceType =
|
||||||
lines[j].split('Device Type:')[1]?.trim() || '';
|
lines[searchIndex].split('Device Type:')[1]?.trim() ||
|
||||||
|
'';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,8 @@ export class IPCHandlers {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle('kobold:getInstalledVersions', () =>
|
||||||
'kobold:getInstalledVersions',
|
this.koboldManager.getInstalledVersions()
|
||||||
(_, includeVersions?: boolean) =>
|
|
||||||
this.koboldManager.getInstalledVersions(includeVersions)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getConfigFiles', () =>
|
ipcMain.handle('kobold:getConfigFiles', () =>
|
||||||
|
|
@ -92,6 +90,10 @@ export class IPCHandlers {
|
||||||
this.koboldManager.getCurrentVersion()
|
this.koboldManager.getCurrentVersion()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:getCurrentBinaryInfo', () =>
|
||||||
|
this.koboldManager.getCurrentBinaryInfo()
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:setCurrentVersion', (_event, version) =>
|
ipcMain.handle('kobold:setCurrentVersion', (_event, version) =>
|
||||||
this.koboldManager.setCurrentVersion(version)
|
this.koboldManager.setCurrentVersion(version)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import type {
|
||||||
|
|
||||||
const koboldAPI: KoboldAPI = {
|
const koboldAPI: KoboldAPI = {
|
||||||
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
|
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
|
||||||
getInstalledVersions: (includeVersions?: boolean) =>
|
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
|
||||||
ipcRenderer.invoke('kobold:getInstalledVersions', includeVersions),
|
|
||||||
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
|
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
|
||||||
|
getCurrentBinaryInfo: () => ipcRenderer.invoke('kobold:getCurrentBinaryInfo'),
|
||||||
setCurrentVersion: (version: string) =>
|
setCurrentVersion: (version: string) =>
|
||||||
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
||||||
getVersionFromBinary: (binaryPath: string) =>
|
getVersionFromBinary: (binaryPath: string) =>
|
||||||
|
|
@ -68,6 +68,14 @@ const koboldAPI: KoboldAPI = {
|
||||||
ipcRenderer.removeListener('install-dir-changed', handler);
|
ipcRenderer.removeListener('install-dir-changed', handler);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
onVersionsUpdated: (callback: () => void) => {
|
||||||
|
const handler = () => callback();
|
||||||
|
ipcRenderer.on('versions-updated', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('versions-updated', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
onKoboldOutput: (callback: (data: string) => void) => {
|
onKoboldOutput: (callback: (data: string) => void) => {
|
||||||
const handler = (_: IpcRendererEvent, data: string) => callback(data);
|
const handler = (_: IpcRendererEvent, data: string) => callback(data);
|
||||||
ipcRenderer.on('kobold-output', handler);
|
ipcRenderer.on('kobold-output', handler);
|
||||||
|
|
|
||||||
9
src/types/electron.d.ts
vendored
9
src/types/electron.d.ts
vendored
|
|
@ -55,10 +55,12 @@ interface ROCmDownload {
|
||||||
|
|
||||||
export interface KoboldAPI {
|
export interface KoboldAPI {
|
||||||
getInstalledVersion: () => Promise<string | undefined>;
|
getInstalledVersion: () => Promise<string | undefined>;
|
||||||
getInstalledVersions: (
|
getInstalledVersions: () => Promise<InstalledVersion[]>;
|
||||||
includeVersions?: boolean
|
|
||||||
) => Promise<InstalledVersion[]>;
|
|
||||||
getCurrentVersion: () => Promise<InstalledVersion | null>;
|
getCurrentVersion: () => Promise<InstalledVersion | null>;
|
||||||
|
getCurrentBinaryInfo: () => Promise<{
|
||||||
|
path: string;
|
||||||
|
filename: string;
|
||||||
|
} | null>;
|
||||||
setCurrentVersion: (version: string) => Promise<boolean>;
|
setCurrentVersion: (version: string) => Promise<boolean>;
|
||||||
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
|
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
|
||||||
getLatestRelease: () => Promise<GitHubRelease>;
|
getLatestRelease: () => Promise<GitHubRelease>;
|
||||||
|
|
@ -105,6 +107,7 @@ export interface KoboldAPI {
|
||||||
onDownloadProgress: (callback: (progress: number) => void) => void;
|
onDownloadProgress: (callback: (progress: number) => void) => void;
|
||||||
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
|
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
|
||||||
onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
|
onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
|
||||||
|
onVersionsUpdated: (callback: () => void) => () => void;
|
||||||
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
onKoboldOutput: (callback: (data: string) => void) => () => void;
|
||||||
removeAllListeners: (channel: string) => void;
|
removeAllListeners: (channel: string) => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue