mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
image generation implementation, standardized tooltips, cache all hardware detection data, better folder structure
This commit is contained in:
parent
21b6f87a53
commit
09f69c7c26
31 changed files with 1466 additions and 512 deletions
13
.github/copilot-instructions.md
vendored
13
.github/copilot-instructions.md
vendored
|
|
@ -1,14 +1,5 @@
|
|||
# Copilot Instructions for FriendlyKobold
|
||||
|
||||
## Code Style Preferences
|
||||
|
||||
### Comments
|
||||
|
||||
- Minimize comments in code - only add them when the code logic is genuinely confusing or complex
|
||||
- Remove obvious/redundant comments that just describe what the code does
|
||||
- Prefer self-documenting code with clear variable and function names
|
||||
- Focus on "why" not "what" when comments are necessary
|
||||
|
||||
### General Coding
|
||||
|
||||
- Follow existing TypeScript/React patterns in the codebase
|
||||
|
|
@ -18,3 +9,7 @@
|
|||
- 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
|
||||
|
||||
### Scripting
|
||||
|
||||
- when debugging: try to run script commands that will work for both bash and fish shells
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ A koboldcpp manager.
|
|||
- 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)
|
||||
- manage the koboldcpp binary to prevent it from running in the background indefinitely
|
||||
- automatically unpack all downloaded koboldcpp binaries for significantly faster operations
|
||||
- automatically unpack all downloaded koboldcpp binaries for significantly faster operation and reduced RAM+HDD utilization (up to ~4GB less RAM usage for ROCm)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
|
|
@ -34,6 +34,10 @@ A koboldcpp manager.
|
|||
|
||||
Additional configurations have been written to help with ideal Wayland support, but as per current Electron guidelines, the user should set `ELECTRON_OZONE_PLATFORM_HINT` to `wayland` in their environment variable according to the [Electron Environment Variables documentation](https://www.electronjs.org/docs/latest/api/environment-variables#electron_ozone_platform_hint-linux).
|
||||
|
||||
### Future features
|
||||
|
||||
Not all koboldcpp features have currently been ported over the UI. As a workaround one may use the "Additional arguments" on the "Advanced" tab of the launcher to provide additional command line arguments if you know them.
|
||||
|
||||
### Future considerations
|
||||
|
||||
It would make a lot of sense to transition this project to Tauri from Electron. The app size should drop from ~80MB to ~10MB; however, users on obsolete OSes (with outdated WebViews) will very likely encounter issues. In addition, I would need to learn Rust to rewrite the BE (Electron main code), but at least we can re-use all the React code. The app would be much smaller, faster and memory efficient, but not work for some users. I think it's a worthy tradeoff.
|
||||
|
|
|
|||
|
|
@ -106,6 +106,13 @@
|
|||
"kobold",
|
||||
"KOBOLDAI",
|
||||
"koboldcpp",
|
||||
"sdmodel",
|
||||
"sdt5xxl",
|
||||
"sdclipl",
|
||||
"sdclipg",
|
||||
"sdphotomaker",
|
||||
"sdvae",
|
||||
"sdapi",
|
||||
"less",
|
||||
"letterspacing",
|
||||
"libvk",
|
||||
|
|
@ -183,6 +190,8 @@
|
|||
"subdomain",
|
||||
"svg",
|
||||
"swiftshader",
|
||||
"safetensors",
|
||||
"SDXL",
|
||||
"tabler",
|
||||
"Tauri",
|
||||
"temp",
|
||||
|
|
|
|||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -11,7 +11,6 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/core": "^8.2.4",
|
||||
"jiti": "^2.5.1",
|
||||
"lucide-react": "^0.539.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
|
|
@ -20,7 +19,7 @@
|
|||
"devDependencies": {
|
||||
"@cspell/eslint-plugin": "^9.2.0",
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
|
|
@ -41,6 +40,7 @@
|
|||
"eslint-plugin-sonarjs": "^3.0.4",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.5.1",
|
||||
"lint-staged": "^16.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
|
|
@ -3227,9 +3227,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -8393,6 +8393,7 @@
|
|||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
"devDependencies": {
|
||||
"@cspell/eslint-plugin": "^9.2.0",
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
|
|
@ -69,6 +69,7 @@
|
|||
"eslint-plugin-sonarjs": "^3.0.4",
|
||||
"globals": "^16.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.5.1",
|
||||
"lint-staged": "^16.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
|
|
@ -79,7 +80,6 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/core": "^8.2.4",
|
||||
"jiti": "^2.5.1",
|
||||
"lucide-react": "^0.539.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
|
|
|
|||
22
src/App.tsx
22
src/App.tsx
|
|
@ -3,7 +3,6 @@ import {
|
|||
AppShell,
|
||||
Group,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
rem,
|
||||
Loader,
|
||||
Center,
|
||||
|
|
@ -15,12 +14,13 @@ import {
|
|||
useMantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { Settings, ArrowLeft } from 'lucide-react';
|
||||
import { DownloadScreen } from '@/components/screens/DownloadScreen';
|
||||
import { LaunchScreen } from '@/components/screens/LaunchScreen';
|
||||
import { InterfaceScreen } from '@/components/screens/InterfaceScreen';
|
||||
import { DownloadScreen } from '@/screens/Download';
|
||||
import { LaunchScreen } from '@/screens/Launch';
|
||||
import { InterfaceScreen } from '@/screens/Interface';
|
||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||
import { SettingsModal } from '@/components/SettingsModal';
|
||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||
import { StyledTooltip } from '@/components/StyledTooltip';
|
||||
import type { UpdateInfo, InstalledVersion } from '@/types';
|
||||
|
||||
type Screen = 'download' | 'launch' | 'interface';
|
||||
|
|
@ -34,6 +34,8 @@ export const App = () => {
|
|||
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
|
||||
'terminal'
|
||||
);
|
||||
const [isImageGenerationMode, setIsImageGenerationMode] =
|
||||
useState<boolean>(false);
|
||||
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
||||
null
|
||||
);
|
||||
|
|
@ -316,7 +318,7 @@ export const App = () => {
|
|||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Settings" position="bottom">
|
||||
<StyledTooltip label="Settings" position="bottom">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
|
|
@ -329,7 +331,7 @@ export const App = () => {
|
|||
>
|
||||
<Settings style={{ width: rem(20), height: rem(20) }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
|
@ -362,7 +364,10 @@ export const App = () => {
|
|||
isActive={currentScreen === 'launch'}
|
||||
shouldAnimate={hasInitialized}
|
||||
>
|
||||
<LaunchScreen onLaunch={handleLaunch} />
|
||||
<LaunchScreen
|
||||
onLaunch={handleLaunch}
|
||||
onLaunchModeChange={setIsImageGenerationMode}
|
||||
/>
|
||||
</ScreenTransition>
|
||||
|
||||
<ScreenTransition
|
||||
|
|
@ -372,6 +377,7 @@ export const App = () => {
|
|||
<InterfaceScreen
|
||||
activeTab={activeInterfaceTab}
|
||||
onTabChange={setActiveInterfaceTab}
|
||||
isImageGenerationMode={isImageGenerationMode}
|
||||
/>
|
||||
</ScreenTransition>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ActionIcon, Tooltip, useMantineColorScheme } from '@mantine/core';
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { Info } from 'lucide-react';
|
||||
import { StyledTooltip } from './StyledTooltip';
|
||||
|
||||
interface InfoTooltipProps {
|
||||
label: string;
|
||||
|
|
@ -11,34 +12,10 @@ export const InfoTooltip = ({
|
|||
label,
|
||||
multiline = true,
|
||||
width = 300,
|
||||
}: InfoTooltipProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={label}
|
||||
multiline={multiline}
|
||||
w={width}
|
||||
withArrow
|
||||
color={isDark ? 'dark' : 'gray'}
|
||||
styles={{
|
||||
tooltip: {
|
||||
backgroundColor: isDark
|
||||
? 'var(--mantine-color-dark-6)'
|
||||
: 'var(--mantine-color-gray-1)',
|
||||
color: isDark
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-7)',
|
||||
border: isDark
|
||||
? '1px solid var(--mantine-color-dark-4)'
|
||||
: '1px solid var(--mantine-color-gray-3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||
<Info size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
}: InfoTooltipProps) => (
|
||||
<StyledTooltip label={label} multiline={multiline} w={width}>
|
||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||
<Info size={14} />
|
||||
</ActionIcon>
|
||||
</StyledTooltip>
|
||||
);
|
||||
|
|
|
|||
35
src/components/StyledTooltip.tsx
Normal file
35
src/components/StyledTooltip.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import { Tooltip, useMantineColorScheme } from '@mantine/core';
|
||||
import type { TooltipProps } from '@mantine/core';
|
||||
|
||||
interface StyledTooltipProps extends Omit<TooltipProps, 'styles' | 'color'> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const StyledTooltip = ({ children, ...props }: StyledTooltipProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
withArrow
|
||||
color={isDark ? 'dark' : 'gray'}
|
||||
styles={{
|
||||
tooltip: {
|
||||
backgroundColor: isDark
|
||||
? 'var(--mantine-color-dark-6)'
|
||||
: 'var(--mantine-color-gray-1)',
|
||||
color: isDark
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-7)',
|
||||
border: isDark
|
||||
? '1px solid var(--mantine-color-dark-4)'
|
||||
: '1px solid var(--mantine-color-gray-3)',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Modal, Tabs, Text, Group, rem } from '@mantine/core';
|
||||
import { Settings, Palette, SlidersHorizontal, GitBranch } from 'lucide-react';
|
||||
import { GeneralTab } from './settings/GeneralTab';
|
||||
import { VersionsTab } from './settings/VersionsTab';
|
||||
import { AppearanceTab } from './settings/AppearanceTab';
|
||||
import { GeneralTab } from './GeneralTab';
|
||||
import { VersionsTab } from './VersionsTab';
|
||||
import { AppearanceTab } from './AppearanceTab';
|
||||
|
||||
interface SettingsModalProps {
|
||||
opened: boolean;
|
||||
|
|
@ -28,6 +28,8 @@ export const SettingsModal = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
setActiveTab('general');
|
||||
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
const scrollbarWidth =
|
||||
window.innerWidth - document.documentElement.clientWidth;
|
||||
|
|
@ -11,13 +11,13 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
import { isAssetCompatibleWithPlatform } from '@/utils/platform';
|
||||
import { getAssetDescription } from '@/utils/assets';
|
||||
import type {
|
||||
InstalledVersion,
|
||||
GitHubAsset,
|
||||
GitHubRelease,
|
||||
} from '@/types/electron';
|
||||
import {
|
||||
getAssetDescription,
|
||||
sortAssetsByRecommendation,
|
||||
isAssetRecommended,
|
||||
} from '@/utils/assets';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import type { InstalledVersion } from '@/types/electron';
|
||||
|
||||
interface VersionInfo {
|
||||
name: string;
|
||||
|
|
@ -31,30 +31,26 @@ interface VersionInfo {
|
|||
}
|
||||
|
||||
export const VersionsTab = () => {
|
||||
const {
|
||||
platformInfo,
|
||||
latestRelease,
|
||||
filteredAssets,
|
||||
rocmDownload,
|
||||
loadingPlatform,
|
||||
loadingRemote,
|
||||
downloading,
|
||||
downloadProgress,
|
||||
loadRemoteVersions,
|
||||
handleDownload: sharedHandleDownload,
|
||||
} = useKoboldVersions();
|
||||
|
||||
const [installedVersions, setInstalledVersions] = useState<
|
||||
InstalledVersion[]
|
||||
>([]);
|
||||
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
||||
null
|
||||
);
|
||||
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>(
|
||||
null
|
||||
);
|
||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
||||
|
||||
const [loadingInstalled, setLoadingInstalled] = useState(true);
|
||||
const [loadingRemote, setLoadingRemote] = useState(true);
|
||||
const [downloading, setDownloading] = useState<string | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<{
|
||||
[key: string]: number;
|
||||
}>({});
|
||||
|
||||
const loadInstalledVersions = useCallback(async () => {
|
||||
setLoadingInstalled(true);
|
||||
|
|
@ -97,68 +93,9 @@ export const VersionsTab = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const loadRemoteVersions = useCallback(async () => {
|
||||
if (!userPlatform) return;
|
||||
setLoadingRemote(true);
|
||||
|
||||
try {
|
||||
const [release, rocm] = await Promise.all([
|
||||
window.electronAPI.kobold.getLatestRelease(),
|
||||
window.electronAPI.kobold.getROCmDownload(),
|
||||
]);
|
||||
|
||||
if (release) {
|
||||
setLatestRelease(release);
|
||||
const compatibleAssets = release.assets.filter((asset) =>
|
||||
isAssetCompatibleWithPlatform(asset.name, userPlatform)
|
||||
);
|
||||
setAvailableAssets(compatibleAssets);
|
||||
}
|
||||
|
||||
setRocmDownload(rocm);
|
||||
} catch (error) {
|
||||
console.error('Failed to load remote versions:', error);
|
||||
} finally {
|
||||
setLoadingRemote(false);
|
||||
}
|
||||
}, [userPlatform]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPlatform = async () => {
|
||||
try {
|
||||
const platform = await window.electronAPI.kobold.getPlatform();
|
||||
setUserPlatform(platform.platform);
|
||||
} catch (error) {
|
||||
console.error('Failed to load platform:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadPlatform();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadInstalledVersions();
|
||||
if (userPlatform) {
|
||||
loadRemoteVersions();
|
||||
}
|
||||
}, [userPlatform, loadInstalledVersions, loadRemoteVersions]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleProgress = (progress: number) => {
|
||||
if (downloading) {
|
||||
setDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
[downloading]: progress,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
window.electronAPI.kobold.onDownloadProgress?.(handleProgress);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.kobold.removeAllListeners?.('download-progress');
|
||||
};
|
||||
}, [downloading]);
|
||||
}, [loadInstalledVersions]);
|
||||
|
||||
const getDisplayNameFromPath = (
|
||||
installedVersion: InstalledVersion
|
||||
|
|
@ -181,7 +118,7 @@ export const VersionsTab = () => {
|
|||
const getAllVersions = (): VersionInfo[] => {
|
||||
const versions: VersionInfo[] = [];
|
||||
|
||||
availableAssets.forEach((asset) => {
|
||||
filteredAssets.forEach((asset) => {
|
||||
const installedVersion = installedVersions.find((v) => {
|
||||
const displayName = getDisplayNameFromPath(v);
|
||||
return displayName === asset.name;
|
||||
|
|
@ -260,38 +197,28 @@ export const VersionsTab = () => {
|
|||
}
|
||||
});
|
||||
|
||||
return versions;
|
||||
return sortAssetsByRecommendation(versions, platformInfo.hasAMDGPU);
|
||||
};
|
||||
|
||||
const handleDownload = async (version: VersionInfo) => {
|
||||
setDownloading(version.name);
|
||||
setDownloadProgress((prev) => ({ ...prev, [version.name]: 0 }));
|
||||
|
||||
try {
|
||||
let result;
|
||||
let success;
|
||||
|
||||
if (version.isROCm) {
|
||||
result = await window.electronAPI.kobold.downloadROCm();
|
||||
success = await sharedHandleDownload('rocm');
|
||||
} else {
|
||||
const asset = availableAssets.find((a) => a.name === version.name);
|
||||
const asset = filteredAssets.find((a) => a.name === version.name);
|
||||
if (!asset) {
|
||||
throw new Error('Asset not found');
|
||||
}
|
||||
result = await window.electronAPI.kobold.downloadRelease(asset);
|
||||
success = await sharedHandleDownload('asset', asset);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
if (success) {
|
||||
await loadInstalledVersions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to download:', error);
|
||||
} finally {
|
||||
setDownloading(null);
|
||||
setDownloadProgress((prev) => {
|
||||
const newProgress = { ...prev };
|
||||
delete newProgress[version.name];
|
||||
return newProgress;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -321,7 +248,7 @@ export const VersionsTab = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const isLoading = loadingInstalled || loadingRemote;
|
||||
const isLoading = loadingInstalled || loadingPlatform || loadingRemote;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -329,7 +256,7 @@ export const VersionsTab = () => {
|
|||
<Stack align="center" gap="md">
|
||||
<Loader size="lg" />
|
||||
<Text c="dimmed">
|
||||
{loadingInstalled && loadingRemote
|
||||
{loadingInstalled && (loadingPlatform || loadingRemote)
|
||||
? 'Loading versions...'
|
||||
: loadingInstalled
|
||||
? 'Scanning installed versions...'
|
||||
|
|
@ -374,6 +301,10 @@ export const VersionsTab = () => {
|
|||
}
|
||||
version={version.version}
|
||||
description={getAssetDescription(version.name)}
|
||||
isRecommended={isAssetRecommended(
|
||||
version.name,
|
||||
platformInfo.hasAMDGPU
|
||||
)}
|
||||
isCurrent={version.isCurrent}
|
||||
isInstalled={version.isInstalled}
|
||||
isDownloading={isDownloading}
|
||||
|
|
|
|||
195
src/hooks/useKoboldVersions.ts
Normal file
195
src/hooks/useKoboldVersions.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { filterAssetsByPlatform } from '@/utils/platform';
|
||||
import type { GitHubAsset, GitHubRelease } from '@/types';
|
||||
|
||||
interface PlatformInfo {
|
||||
platform: string;
|
||||
hasAMDGPU: boolean;
|
||||
hasROCm: boolean;
|
||||
}
|
||||
|
||||
interface ROCmDownload {
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
interface UseKoboldVersionsReturn {
|
||||
platformInfo: PlatformInfo;
|
||||
latestRelease: GitHubRelease | null;
|
||||
filteredAssets: GitHubAsset[];
|
||||
rocmDownload: ROCmDownload | null;
|
||||
loadingPlatform: boolean;
|
||||
loadingRemote: boolean;
|
||||
downloading: string | null;
|
||||
downloadProgress: Record<string, number>;
|
||||
loadRemoteVersions: () => Promise<void>;
|
||||
handleDownload: (
|
||||
type: 'asset' | 'rocm',
|
||||
asset?: GitHubAsset
|
||||
) => Promise<boolean>;
|
||||
setDownloading: (value: string | null) => void;
|
||||
setDownloadProgress: (
|
||||
value:
|
||||
| Record<string, number>
|
||||
| ((prev: Record<string, number>) => Record<string, number>)
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useKoboldVersions = (): UseKoboldVersionsReturn => {
|
||||
const [platformInfo, setPlatformInfo] = useState<PlatformInfo>({
|
||||
platform: '',
|
||||
hasAMDGPU: false,
|
||||
hasROCm: false,
|
||||
});
|
||||
|
||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
||||
null
|
||||
);
|
||||
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
|
||||
const [rocmDownload, setRocmDownload] = useState<ROCmDownload | null>(null);
|
||||
|
||||
const [loadingPlatform, setLoadingPlatform] = useState(true);
|
||||
const [loadingRemote, setLoadingRemote] = useState(true);
|
||||
|
||||
const [downloading, setDownloading] = useState<string | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<
|
||||
Record<string, number>
|
||||
>({});
|
||||
|
||||
const loadPlatformInfo = useCallback(async () => {
|
||||
setLoadingPlatform(true);
|
||||
|
||||
try {
|
||||
const platform = await window.electronAPI.kobold.getPlatform();
|
||||
|
||||
let hasAMDGPU = false;
|
||||
let hasROCm = false;
|
||||
|
||||
try {
|
||||
const gpuInfo = await window.electronAPI.kobold.detectGPU();
|
||||
hasAMDGPU = gpuInfo.hasAMD;
|
||||
|
||||
if (gpuInfo.hasAMD) {
|
||||
const rocmInfo = await window.electronAPI.kobold.detectROCm();
|
||||
hasROCm = rocmInfo.supported;
|
||||
}
|
||||
} catch (gpuError) {
|
||||
console.warn('GPU detection failed:', gpuError);
|
||||
}
|
||||
|
||||
setPlatformInfo({
|
||||
platform: platform.platform,
|
||||
hasAMDGPU,
|
||||
hasROCm,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load platform info:', error);
|
||||
} finally {
|
||||
setLoadingPlatform(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadRemoteVersions = useCallback(async () => {
|
||||
if (!platformInfo.platform) return;
|
||||
|
||||
setLoadingRemote(true);
|
||||
|
||||
try {
|
||||
const [release, rocm] = await Promise.all([
|
||||
window.electronAPI.kobold.getLatestRelease(),
|
||||
window.electronAPI.kobold.getROCmDownload(),
|
||||
]);
|
||||
|
||||
setLatestRelease(release);
|
||||
setRocmDownload(rocm);
|
||||
|
||||
if (release) {
|
||||
const filtered = filterAssetsByPlatform(
|
||||
release.assets,
|
||||
platformInfo.platform
|
||||
);
|
||||
setFilteredAssets(filtered);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load remote versions:', error);
|
||||
} finally {
|
||||
setLoadingRemote(false);
|
||||
}
|
||||
}, [platformInfo.platform]);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
async (type: 'asset' | 'rocm', asset?: GitHubAsset): Promise<boolean> => {
|
||||
if (type === 'asset' && !asset) return false;
|
||||
|
||||
const downloadName =
|
||||
type === 'asset' ? asset!.name : rocmDownload?.name || 'rocm';
|
||||
|
||||
setDownloading(downloadName);
|
||||
setDownloadProgress((prev) => ({ ...prev, [downloadName]: 0 }));
|
||||
|
||||
try {
|
||||
const result =
|
||||
type === 'rocm'
|
||||
? await window.electronAPI.kobold.downloadROCm()
|
||||
: await window.electronAPI.kobold.downloadRelease(asset!);
|
||||
|
||||
return result.success !== false;
|
||||
} catch (error) {
|
||||
console.error(`Failed to download ${type}:`, error);
|
||||
return false;
|
||||
} finally {
|
||||
setDownloading(null);
|
||||
setDownloadProgress((prev) => {
|
||||
const newProgress = { ...prev };
|
||||
delete newProgress[downloadName];
|
||||
return newProgress;
|
||||
});
|
||||
}
|
||||
},
|
||||
[rocmDownload?.name]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadPlatformInfo();
|
||||
}, [loadPlatformInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (platformInfo.platform) {
|
||||
loadRemoteVersions();
|
||||
}
|
||||
}, [platformInfo.platform, loadRemoteVersions]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleProgress = (progress: number) => {
|
||||
if (downloading) {
|
||||
setDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
[downloading]: progress,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
window.electronAPI.kobold.onDownloadProgress?.(handleProgress);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.kobold.removeAllListeners?.('download-progress');
|
||||
};
|
||||
}, [downloading]);
|
||||
|
||||
return {
|
||||
platformInfo,
|
||||
latestRelease,
|
||||
filteredAssets,
|
||||
rocmDownload,
|
||||
loadingPlatform,
|
||||
loadingRemote,
|
||||
downloading,
|
||||
downloadProgress,
|
||||
loadRemoteVersions,
|
||||
handleDownload,
|
||||
setDownloading,
|
||||
setDownloadProgress,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type { ConfigFile } from '@/types';
|
||||
import {
|
||||
getPresetByName,
|
||||
type ImageModelPreset,
|
||||
} from '@/utils/imageModelPresets';
|
||||
|
||||
export const useLaunchConfig = () => {
|
||||
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
||||
|
|
@ -21,10 +25,24 @@ export const useLaunchConfig = () => {
|
|||
const [failsafe, setFailsafe] = useState<boolean>(false);
|
||||
const [backend, setBackend] = useState<string>('cpu');
|
||||
|
||||
const [sdmodel, setSdmodel] = useState<string>('');
|
||||
const [sdt5xxl, setSdt5xxl] = useState<string>(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true'
|
||||
);
|
||||
const [sdclipl, setSdclipl] = useState<string>(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true'
|
||||
);
|
||||
const [sdclipg, setSdclipg] = useState<string>('');
|
||||
const [sdphotomaker, setSdphotomaker] = useState<string>('');
|
||||
const [sdvae, setSdvae] = useState<string>(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true'
|
||||
);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||
const configData =
|
||||
await window.electronAPI.kobold.parseConfigFile(configPath);
|
||||
|
||||
if (configData) {
|
||||
if (typeof configData.gpulayers === 'number') {
|
||||
setGpuLayers(configData.gpulayers);
|
||||
|
|
@ -124,7 +142,32 @@ export const useLaunchConfig = () => {
|
|||
} else {
|
||||
setBackend('cpu');
|
||||
}
|
||||
|
||||
if (typeof configData.sdmodel === 'string') {
|
||||
setSdmodel(configData.sdmodel);
|
||||
}
|
||||
|
||||
if (typeof configData.sdt5xxl === 'string') {
|
||||
setSdt5xxl(configData.sdt5xxl);
|
||||
}
|
||||
|
||||
if (typeof configData.sdclipl === 'string') {
|
||||
setSdclipl(configData.sdclipl);
|
||||
}
|
||||
|
||||
if (typeof configData.sdclipg === 'string') {
|
||||
setSdclipg(configData.sdclipg);
|
||||
}
|
||||
|
||||
if (typeof configData.sdphotomaker === 'string') {
|
||||
setSdphotomaker(configData.sdphotomaker);
|
||||
}
|
||||
|
||||
if (typeof configData.sdvae === 'string') {
|
||||
setSdvae(configData.sdvae);
|
||||
}
|
||||
} else {
|
||||
const cpuCapabilities = await window.electronAPI.kobold.detectCPU();
|
||||
setGpuLayers(0);
|
||||
setContextSize(2048);
|
||||
setPort(5001);
|
||||
|
|
@ -136,14 +179,30 @@ export const useLaunchConfig = () => {
|
|||
setWebsearch(false);
|
||||
setNoshift(false);
|
||||
setFlashattention(false);
|
||||
setNoavx2(false);
|
||||
setFailsafe(false);
|
||||
setNoavx2(!cpuCapabilities.avx2);
|
||||
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
|
||||
setBackend('cpu');
|
||||
|
||||
setSdmodel('');
|
||||
setSdt5xxl(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true'
|
||||
);
|
||||
setSdclipl(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true'
|
||||
);
|
||||
setSdclipg('');
|
||||
setSdphotomaker('');
|
||||
setSdvae(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true'
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSavedSettings = useCallback(async () => {
|
||||
const savedServerOnly = await window.electronAPI.config.getServerOnly();
|
||||
const [savedServerOnly, cpuCapabilities] = await Promise.all([
|
||||
window.electronAPI.config.getServerOnly(),
|
||||
window.electronAPI.kobold.detectCPU(),
|
||||
]);
|
||||
|
||||
setServerOnly(savedServerOnly);
|
||||
setModelPath('');
|
||||
|
|
@ -158,9 +217,22 @@ export const useLaunchConfig = () => {
|
|||
setWebsearch(false);
|
||||
setNoshift(false);
|
||||
setFlashattention(false);
|
||||
setNoavx2(false);
|
||||
setFailsafe(false);
|
||||
setNoavx2(!cpuCapabilities.avx2);
|
||||
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
|
||||
setBackend('cpu');
|
||||
|
||||
setSdmodel('');
|
||||
setSdt5xxl(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true'
|
||||
);
|
||||
setSdclipl(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true'
|
||||
);
|
||||
setSdclipg('');
|
||||
setSdphotomaker('');
|
||||
setSdvae(
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true'
|
||||
);
|
||||
}, []);
|
||||
|
||||
const loadConfigFromFile = useCallback(
|
||||
|
|
@ -278,6 +350,91 @@ export const useLaunchConfig = () => {
|
|||
setBackend(backend);
|
||||
}, []);
|
||||
|
||||
const handleSdmodelChange = useCallback((path: string) => {
|
||||
setSdmodel(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectSdmodelFile = useCallback(async () => {
|
||||
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||
if (filePath) {
|
||||
setSdmodel(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSdt5xxlChange = useCallback((path: string) => {
|
||||
setSdt5xxl(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectSdt5xxlFile = useCallback(async () => {
|
||||
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||
if (filePath) {
|
||||
setSdt5xxl(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSdcliplChange = useCallback((path: string) => {
|
||||
setSdclipl(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectSdcliplFile = useCallback(async () => {
|
||||
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||
if (filePath) {
|
||||
setSdclipl(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSdclipgChange = useCallback((path: string) => {
|
||||
setSdclipg(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectSdclipgFile = useCallback(async () => {
|
||||
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||
if (filePath) {
|
||||
setSdclipg(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSdphotomakerChange = useCallback((path: string) => {
|
||||
setSdphotomaker(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectSdphotomakerFile = useCallback(async () => {
|
||||
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||
if (filePath) {
|
||||
setSdphotomaker(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSdvaeChange = useCallback((path: string) => {
|
||||
setSdvae(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectSdvaeFile = useCallback(async () => {
|
||||
const filePath = await window.electronAPI.kobold.selectModelFile();
|
||||
if (filePath) {
|
||||
setSdvae(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const applyImageModelPreset = useCallback((preset: ImageModelPreset) => {
|
||||
setSdmodel(preset.sdmodel);
|
||||
setSdt5xxl(preset.sdt5xxl);
|
||||
setSdclipl(preset.sdclipl);
|
||||
setSdclipg(preset.sdclipg);
|
||||
setSdphotomaker(preset.sdphotomaker);
|
||||
setSdvae(preset.sdvae);
|
||||
}, []);
|
||||
|
||||
const handleApplyPreset = useCallback(
|
||||
(presetName: string) => {
|
||||
const preset = getPresetByName(presetName);
|
||||
if (preset) {
|
||||
applyImageModelPreset(preset);
|
||||
}
|
||||
},
|
||||
[applyImageModelPreset]
|
||||
);
|
||||
|
||||
return {
|
||||
serverOnly,
|
||||
gpuLayers,
|
||||
|
|
@ -297,6 +454,12 @@ export const useLaunchConfig = () => {
|
|||
noavx2,
|
||||
failsafe,
|
||||
backend,
|
||||
sdmodel,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
|
||||
parseAndApplyConfigFile,
|
||||
loadSavedSettings,
|
||||
|
|
@ -320,5 +483,19 @@ export const useLaunchConfig = () => {
|
|||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleBackendChange,
|
||||
handleSdmodelChange,
|
||||
handleSelectSdmodelFile,
|
||||
handleSdt5xxlChange,
|
||||
handleSelectSdt5xxlFile,
|
||||
handleSdcliplChange,
|
||||
handleSelectSdcliplFile,
|
||||
handleSdclipgChange,
|
||||
handleSelectSdclipgFile,
|
||||
handleSdphotomakerChange,
|
||||
handleSelectSdphotomakerFile,
|
||||
handleSdvaeChange,
|
||||
handleSelectSdvaeFile,
|
||||
applyImageModelPreset,
|
||||
handleApplyPreset,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
createWriteStream,
|
||||
chmodSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from 'fs';
|
||||
import { dialog } from 'electron';
|
||||
|
|
@ -334,6 +335,52 @@ export class KoboldCppManager {
|
|||
}
|
||||
}
|
||||
|
||||
async saveConfigFile(
|
||||
configName: string,
|
||||
configData: {
|
||||
gpulayers?: number;
|
||||
contextsize?: number;
|
||||
model_param?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
multiuser?: number;
|
||||
multiplayer?: boolean;
|
||||
remotetunnel?: boolean;
|
||||
nocertify?: boolean;
|
||||
websearch?: boolean;
|
||||
noshift?: boolean;
|
||||
flashattention?: boolean;
|
||||
noavx2?: boolean;
|
||||
failsafe?: boolean;
|
||||
usecuda?: boolean;
|
||||
usevulkan?: boolean;
|
||||
useclblast?: boolean;
|
||||
sdmodel?: string;
|
||||
sdt5xxl?: string;
|
||||
sdclipl?: string;
|
||||
sdclipg?: string;
|
||||
sdphotomaker?: string;
|
||||
sdvae?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!this.installDir) {
|
||||
console.error('No install directory found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const configFileName = `${configName}.json`;
|
||||
const configPath = join(this.installDir, configFileName);
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving config file:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async selectModelFile(): Promise<string | null> {
|
||||
try {
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-comments/disallowComments */
|
||||
import si from 'systeminformation';
|
||||
import type {
|
||||
CPUCapabilities,
|
||||
|
|
@ -7,7 +8,15 @@ import type {
|
|||
} from '@/types/hardware';
|
||||
|
||||
export class HardwareService {
|
||||
private cpuCapabilitiesCache: CPUCapabilities | null = null;
|
||||
private basicGPUInfoCache: BasicGPUInfo | null = null;
|
||||
private gpuCapabilitiesCache: GPUCapabilities | null = null;
|
||||
|
||||
async detectCPU(): Promise<CPUCapabilities> {
|
||||
if (this.cpuCapabilitiesCache) {
|
||||
return this.cpuCapabilitiesCache;
|
||||
}
|
||||
|
||||
try {
|
||||
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
|
||||
|
||||
|
|
@ -25,22 +34,30 @@ export class HardwareService {
|
|||
const avx = flags.includes('avx') || flags.includes('AVX');
|
||||
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
||||
|
||||
return {
|
||||
this.cpuCapabilitiesCache = {
|
||||
avx,
|
||||
avx2,
|
||||
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'],
|
||||
};
|
||||
|
||||
return this.cpuCapabilitiesCache;
|
||||
} catch (error) {
|
||||
console.warn('CPU detection failed:', error);
|
||||
return {
|
||||
const fallbackCapabilities = {
|
||||
avx: false,
|
||||
avx2: false,
|
||||
cpuInfo: ['CPU detection failed'],
|
||||
};
|
||||
this.cpuCapabilitiesCache = fallbackCapabilities;
|
||||
return fallbackCapabilities;
|
||||
}
|
||||
}
|
||||
|
||||
async detectGPU(): Promise<BasicGPUInfo> {
|
||||
if (this.basicGPUInfoCache) {
|
||||
return this.basicGPUInfoCache;
|
||||
}
|
||||
|
||||
try {
|
||||
const graphics = await si.graphics();
|
||||
|
||||
|
|
@ -76,23 +93,33 @@ export class HardwareService {
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
this.basicGPUInfoCache = {
|
||||
hasAMD,
|
||||
hasNVIDIA,
|
||||
gpuInfo:
|
||||
gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||
};
|
||||
|
||||
return this.basicGPUInfoCache;
|
||||
} catch (error) {
|
||||
console.warn('GPU detection failed:', error);
|
||||
return {
|
||||
const fallbackGPUInfo = {
|
||||
hasAMD: false,
|
||||
hasNVIDIA: false,
|
||||
gpuInfo: ['GPU detection failed'],
|
||||
};
|
||||
this.basicGPUInfoCache = fallbackGPUInfo;
|
||||
return fallbackGPUInfo;
|
||||
}
|
||||
}
|
||||
|
||||
async detectGPUCapabilities(): Promise<GPUCapabilities> {
|
||||
// WARNING: we're not worrying about the users that update their system
|
||||
// during runtime and not restart. Should we be though?
|
||||
if (this.gpuCapabilitiesCache) {
|
||||
return this.gpuCapabilitiesCache;
|
||||
}
|
||||
|
||||
const [cuda, rocm, vulkan, clblast] = await Promise.all([
|
||||
this.detectCUDA(),
|
||||
this.detectROCm(),
|
||||
|
|
@ -100,7 +127,8 @@ export class HardwareService {
|
|||
this.detectCLBlast(),
|
||||
]);
|
||||
|
||||
return { cuda, rocm, vulkan, clblast };
|
||||
this.gpuCapabilitiesCache = { cuda, rocm, vulkan, clblast };
|
||||
return this.gpuCapabilitiesCache;
|
||||
}
|
||||
|
||||
async detectAll(): Promise<HardwareInfo> {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ export class IPCHandlers {
|
|||
this.koboldManager.getConfigFiles()
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
'kobold:saveConfigFile',
|
||||
async (_event, configName, configData) =>
|
||||
this.koboldManager.saveConfigFile(configName, configData)
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:getSelectedConfig', () =>
|
||||
this.configManager.getSelectedConfig()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,35 @@ const koboldAPI: KoboldAPI = {
|
|||
openInstallDialog: () => ipcRenderer.invoke('kobold:openInstallDialog'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
||||
saveConfigFile: (
|
||||
configName: string,
|
||||
configData: {
|
||||
gpulayers?: number;
|
||||
contextsize?: number;
|
||||
model_param?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
multiuser?: number;
|
||||
multiplayer?: boolean;
|
||||
remotetunnel?: boolean;
|
||||
nocertify?: boolean;
|
||||
websearch?: boolean;
|
||||
noshift?: boolean;
|
||||
flashattention?: boolean;
|
||||
noavx2?: boolean;
|
||||
failsafe?: boolean;
|
||||
usecuda?: boolean;
|
||||
usevulkan?: boolean;
|
||||
useclblast?: boolean;
|
||||
sdmodel?: string;
|
||||
sdt5xxl?: string;
|
||||
sdclipl?: string;
|
||||
sdclipg?: string;
|
||||
sdphotomaker?: string;
|
||||
sdvae?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
) => ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
|
||||
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
||||
setSelectedConfig: (configName: string) =>
|
||||
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
|
|
@ -7,13 +7,10 @@ import {
|
|||
Stack,
|
||||
Container,
|
||||
Badge,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
import {
|
||||
getPlatformDisplayName,
|
||||
filterAssetsByPlatform,
|
||||
} from '@/utils/platform';
|
||||
import { StyledTooltip } from '@/components/StyledTooltip';
|
||||
import { getPlatformDisplayName } from '@/utils/platform';
|
||||
import { formatFileSize } from '@/utils/fileSize';
|
||||
import {
|
||||
isAssetRecommended,
|
||||
|
|
@ -21,127 +18,59 @@ import {
|
|||
getAssetDescription,
|
||||
} from '@/utils/assets';
|
||||
import { ROCM } from '@/constants';
|
||||
import type { GitHubAsset, GitHubRelease } from '@/types';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import type { GitHubAsset } from '@/types';
|
||||
|
||||
interface DownloadScreenProps {
|
||||
onDownloadComplete: () => void;
|
||||
}
|
||||
|
||||
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
||||
null
|
||||
);
|
||||
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
|
||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
||||
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
|
||||
const [hasROCm, setHasROCm] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const {
|
||||
platformInfo,
|
||||
latestRelease,
|
||||
filteredAssets,
|
||||
rocmDownload,
|
||||
loadingPlatform,
|
||||
loadingRemote,
|
||||
downloading,
|
||||
downloadProgress,
|
||||
handleDownload: sharedHandleDownload,
|
||||
} = useKoboldVersions();
|
||||
|
||||
const [downloadingType, setDownloadingType] = useState<
|
||||
'asset' | 'rocm' | null
|
||||
>(null);
|
||||
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
|
||||
const [rocmDownload, setRocmDownload] = useState<{
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
version?: string;
|
||||
} | null>(null);
|
||||
|
||||
const loadLatestReleaseAndPlatform = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const [platformInfo, releaseData, rocmDownloadInfo] = await Promise.all([
|
||||
window.electronAPI.kobold.getPlatform(),
|
||||
window.electronAPI.kobold.getLatestRelease(),
|
||||
window.electronAPI.kobold.getROCmDownload(),
|
||||
]);
|
||||
|
||||
setUserPlatform(platformInfo.platform);
|
||||
setLatestRelease(releaseData);
|
||||
setRocmDownload(rocmDownloadInfo);
|
||||
|
||||
try {
|
||||
const gpuInfo = await window.electronAPI.kobold.detectGPU();
|
||||
setHasAMDGPU(gpuInfo.hasAMD);
|
||||
|
||||
if (gpuInfo.hasAMD) {
|
||||
const rocmInfo = await window.electronAPI.kobold.detectROCm();
|
||||
setHasROCm(rocmInfo.supported);
|
||||
}
|
||||
} catch (gpuError) {
|
||||
console.warn(
|
||||
'GPU detection failed, proceeding without GPU info:',
|
||||
gpuError
|
||||
);
|
||||
setHasAMDGPU(false);
|
||||
setHasROCm(false);
|
||||
}
|
||||
|
||||
if (releaseData) {
|
||||
const filtered = filterAssetsByPlatform(
|
||||
releaseData.assets,
|
||||
platformInfo.platform
|
||||
);
|
||||
setFilteredAssets(filtered);
|
||||
} else {
|
||||
console.error(
|
||||
'GitHub API is currently unavailable. Please try again later.'
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load release information:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadLatestReleaseAndPlatform();
|
||||
|
||||
window.electronAPI.kobold.onDownloadProgress((progress: number) => {
|
||||
setDownloadProgress(progress);
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.electronAPI.kobold.removeAllListeners('download-progress');
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const loading = loadingPlatform || loadingRemote;
|
||||
|
||||
const handleDownload = useCallback(
|
||||
async (type: 'asset' | 'rocm', asset?: GitHubAsset) => {
|
||||
if (type === 'asset' && !asset) return;
|
||||
|
||||
setDownloading(true);
|
||||
setDownloadingType(type);
|
||||
setDownloadingAsset(type === 'asset' ? asset!.name : null);
|
||||
setDownloadProgress(0);
|
||||
|
||||
try {
|
||||
await (type === 'rocm'
|
||||
? window.electronAPI.kobold.downloadROCm()
|
||||
: await window.electronAPI.kobold.downloadRelease(asset!));
|
||||
const success = await sharedHandleDownload(type, asset);
|
||||
|
||||
onDownloadComplete();
|
||||
if (success) {
|
||||
onDownloadComplete();
|
||||
|
||||
setTimeout(() => {
|
||||
setDownloading(false);
|
||||
setDownloadingType(null);
|
||||
setDownloadingAsset(null);
|
||||
setDownloadProgress(0);
|
||||
}, 200);
|
||||
setTimeout(() => {
|
||||
setDownloadingType(null);
|
||||
setDownloadingAsset(null);
|
||||
}, 200);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to download ${type}:`, error);
|
||||
setDownloading(false);
|
||||
} finally {
|
||||
setDownloadingType(null);
|
||||
setDownloadingAsset(null);
|
||||
setDownloadProgress(0);
|
||||
}
|
||||
},
|
||||
[onDownloadComplete]
|
||||
[sharedHandleDownload, onDownloadComplete]
|
||||
);
|
||||
|
||||
const renderROCmCard = () => {
|
||||
|
|
@ -152,10 +81,17 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
name={ROCM.BINARY_NAME}
|
||||
size={formatFileSize(ROCM.SIZE_BYTES)}
|
||||
description={getAssetDescription(ROCM.BINARY_NAME)}
|
||||
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
|
||||
isDownloading={downloading && downloadingType === 'rocm'}
|
||||
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
|
||||
disabled={downloading && downloadingType !== 'rocm'}
|
||||
isRecommended={isAssetRecommended(
|
||||
ROCM.BINARY_NAME,
|
||||
platformInfo.hasAMDGPU
|
||||
)}
|
||||
isDownloading={Boolean(downloading) && downloadingType === 'rocm'}
|
||||
downloadProgress={
|
||||
downloadingType === 'rocm'
|
||||
? downloadProgress[ROCM.BINARY_NAME] || 0
|
||||
: 0
|
||||
}
|
||||
disabled={Boolean(downloading) && downloadingType !== 'rocm'}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload('rocm');
|
||||
|
|
@ -208,7 +144,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
|
||||
{filteredAssets.length > 0 || rocmDownload ? (
|
||||
<Stack gap="sm">
|
||||
{hasAMDGPU && !hasROCm && (
|
||||
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
|
||||
<Card withBorder p="md" bg="orange.0">
|
||||
<Stack gap="xs">
|
||||
<div
|
||||
|
|
@ -221,7 +157,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
<Text fw={600} c="orange.9">
|
||||
AMD GPU Detected
|
||||
</Text>
|
||||
<Tooltip
|
||||
<StyledTooltip
|
||||
label="ROCm is not installed. Install ROCm for optimal AMD GPU performance with KoboldCpp."
|
||||
multiline
|
||||
w={220}
|
||||
|
|
@ -233,7 +169,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
>
|
||||
ROCm Not Found
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
<Text size="sm" c="orange.8">
|
||||
For best performance with your AMD GPU, consider
|
||||
|
|
@ -243,11 +179,13 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{rocmDownload && hasAMDGPU && renderROCmCard()}
|
||||
{rocmDownload &&
|
||||
platformInfo.hasAMDGPU &&
|
||||
renderROCmCard()}
|
||||
|
||||
{sortAssetsByRecommendation(
|
||||
filteredAssets,
|
||||
hasAMDGPU
|
||||
platformInfo.hasAMDGPU
|
||||
).map((asset) => (
|
||||
<DownloadCard
|
||||
key={asset.name}
|
||||
|
|
@ -256,22 +194,23 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
description={getAssetDescription(asset.name)}
|
||||
isRecommended={isAssetRecommended(
|
||||
asset.name,
|
||||
hasAMDGPU
|
||||
platformInfo.hasAMDGPU
|
||||
)}
|
||||
isDownloading={
|
||||
downloading &&
|
||||
Boolean(downloading) &&
|
||||
downloadingType === 'asset' &&
|
||||
downloadingAsset === asset.name
|
||||
}
|
||||
downloadProgress={
|
||||
downloading &&
|
||||
Boolean(downloading) &&
|
||||
downloadingType === 'asset' &&
|
||||
downloadingAsset === asset.name
|
||||
? downloadProgress
|
||||
? downloadProgress[asset.name] || 0
|
||||
: 0
|
||||
}
|
||||
disabled={
|
||||
downloading && downloadingAsset !== asset.name
|
||||
Boolean(downloading) &&
|
||||
downloadingAsset !== asset.name
|
||||
}
|
||||
onDownload={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -280,7 +219,9 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
/>
|
||||
))}
|
||||
|
||||
{rocmDownload && !hasAMDGPU && renderROCmCard()}
|
||||
{rocmDownload &&
|
||||
!platformInfo.hasAMDGPU &&
|
||||
renderROCmCard()}
|
||||
</Stack>
|
||||
) : (
|
||||
<Card withBorder p="md" bg="yellow.0" c="yellow.9">
|
||||
|
|
@ -288,7 +229,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
<Text fw={500}>No downloads available</Text>
|
||||
<Text size="sm">
|
||||
No downloads available for your platform (
|
||||
{getPlatformDisplayName(userPlatform)}).
|
||||
{getPlatformDisplayName(platformInfo.platform)}).
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
import { useRef } from 'react';
|
||||
import { Box, Text, Stack } from '@mantine/core';
|
||||
|
||||
interface ChatTabProps {
|
||||
interface ImageGenerationTabProps {
|
||||
serverUrl?: string;
|
||||
isServerReady?: boolean;
|
||||
}
|
||||
|
||||
export const ChatTab = ({ serverUrl, isServerReady }: ChatTabProps) => {
|
||||
export const ImageGenerationTab = ({
|
||||
serverUrl,
|
||||
isServerReady,
|
||||
}: ImageGenerationTabProps) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
if (!isServerReady || !serverUrl) {
|
||||
|
|
@ -25,19 +28,21 @@ export const ChatTab = ({ serverUrl, isServerReady }: ChatTabProps) => {
|
|||
Waiting for KoboldCpp server to start...
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
The chat interface will load automatically when ready
|
||||
The image generation interface will load automatically when ready
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const imageGenerationUrl = `${serverUrl}/sdapi/v1`;
|
||||
|
||||
return (
|
||||
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={serverUrl}
|
||||
title="KoboldAI Lite Interface"
|
||||
src={imageGenerationUrl}
|
||||
title="KoboldCpp Image Generation Interface"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
64
src/screens/Interface/ServerTab.tsx
Normal file
64
src/screens/Interface/ServerTab.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { useRef } from 'react';
|
||||
import { Box, Text, Stack } from '@mantine/core';
|
||||
|
||||
interface ServerTabProps {
|
||||
serverUrl?: string;
|
||||
isServerReady?: boolean;
|
||||
mode: 'chat' | 'image-generation';
|
||||
}
|
||||
|
||||
export const ServerTab = ({
|
||||
serverUrl,
|
||||
isServerReady,
|
||||
mode,
|
||||
}: ServerTabProps) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
if (!isServerReady || !serverUrl) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="md">
|
||||
<Text c="dimmed" size="lg">
|
||||
Waiting for KoboldCpp server to start...
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
The {mode === 'chat' ? 'chat' : 'image generation'} interface will
|
||||
load automatically when ready
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const iframeUrl =
|
||||
mode === 'image-generation' ? `${serverUrl}/sdapi/v1` : serverUrl;
|
||||
const title =
|
||||
mode === 'image-generation'
|
||||
? 'KoboldCpp Image Generation Interface'
|
||||
: 'KoboldAI Lite Interface';
|
||||
|
||||
return (
|
||||
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
title={title}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
borderRadius: 'inherit',
|
||||
}}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Container } from '@mantine/core';
|
||||
import { ChatTab } from '@/components/interface/ChatTab';
|
||||
import { TerminalTab } from '@/components/interface/TerminalTab';
|
||||
import { ServerTab } from '@/screens/Interface/ServerTab';
|
||||
import { TerminalTab } from '@/screens/Interface/TerminalTab';
|
||||
|
||||
interface InterfaceScreenProps {
|
||||
activeTab?: string | null;
|
||||
onTabChange?: (tab: string | null) => void;
|
||||
isImageGenerationMode?: boolean;
|
||||
}
|
||||
|
||||
export const InterfaceScreen = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
isImageGenerationMode = false,
|
||||
}: InterfaceScreenProps) => {
|
||||
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
||||
const [serverUrl, setServerUrl] = useState<string>('');
|
||||
|
|
@ -22,10 +24,10 @@ export const InterfaceScreen = ({
|
|||
setIsServerReady(true);
|
||||
|
||||
if (!serverOnly && onTabChange) {
|
||||
onTabChange('chat');
|
||||
onTabChange(isImageGenerationMode ? 'image' : 'chat');
|
||||
}
|
||||
},
|
||||
[serverOnly, onTabChange]
|
||||
[serverOnly, onTabChange, isImageGenerationMode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -66,7 +68,23 @@ export const InterfaceScreen = ({
|
|||
display: activeTab === 'chat' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ChatTab serverUrl={serverUrl} isServerReady={isServerReady} />
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: activeTab === 'image' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode="image-generation"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -31,22 +31,6 @@ export const AdvancedTab = ({
|
|||
onFailsafeChange,
|
||||
}: AdvancedTabProps) => (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Additional arguments
|
||||
</Text>
|
||||
<InfoTooltip label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are." />
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder="Additional command line arguments"
|
||||
value={additionalArguments}
|
||||
onChange={(event) =>
|
||||
onAdditionalArgumentsChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Stack gap="md">
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
|
|
@ -115,5 +99,20 @@ export const AdvancedTab = ({
|
|||
</Group>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Additional arguments
|
||||
</Text>
|
||||
<InfoTooltip label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are." />
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder="Additional command line arguments"
|
||||
value={additionalArguments}
|
||||
onChange={(event) =>
|
||||
onAdditionalArgumentsChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
213
src/screens/Launch/BackendSelector.tsx
Normal file
213
src/screens/Launch/BackendSelector.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import {
|
||||
Text,
|
||||
Group,
|
||||
Select,
|
||||
Badge,
|
||||
Card,
|
||||
useMantineColorScheme,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { isNoCudaBinary, isRocmBinary } from '@/utils/binaryUtils';
|
||||
|
||||
interface BackendSelectorProps {
|
||||
backend: string;
|
||||
onBackendChange: (backend: string) => void;
|
||||
noavx2?: boolean;
|
||||
failsafe?: boolean;
|
||||
}
|
||||
|
||||
export const BackendSelector = ({
|
||||
backend,
|
||||
onBackendChange,
|
||||
noavx2 = false,
|
||||
failsafe = false,
|
||||
}: BackendSelectorProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
const [availableBackends, setAvailableBackends] = useState<
|
||||
Array<{ value: string; label: string; devices?: string[] }>
|
||||
>([]);
|
||||
const [isLoadingBackends, setIsLoadingBackends] = useState(true);
|
||||
const [cpuCapabilities, setCpuCapabilities] = useState<{
|
||||
avx: boolean;
|
||||
avx2: boolean;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const detectAvailableBackends = async () => {
|
||||
setIsLoadingBackends(true);
|
||||
|
||||
try {
|
||||
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentBinaryInfo(),
|
||||
window.electronAPI.kobold.detectCPU(),
|
||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||
]);
|
||||
|
||||
setCpuCapabilities({
|
||||
avx: cpuCapabilitiesResult.avx,
|
||||
avx2: cpuCapabilitiesResult.avx2,
|
||||
});
|
||||
|
||||
const backends: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
devices?: string[];
|
||||
}> = [];
|
||||
|
||||
if (currentBinaryInfo?.filename) {
|
||||
const filename = currentBinaryInfo.filename;
|
||||
|
||||
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) {
|
||||
backends.push({
|
||||
value: 'cuda',
|
||||
label: 'CUDA',
|
||||
devices: gpuCapabilities.cuda.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (isRocmBinary(filename) && gpuCapabilities.rocm.supported) {
|
||||
backends.push({
|
||||
value: 'rocm',
|
||||
label: 'ROCm',
|
||||
devices: gpuCapabilities.rocm.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.vulkan.supported) {
|
||||
backends.push({
|
||||
value: 'vulkan',
|
||||
label: 'Vulkan',
|
||||
devices: gpuCapabilities.vulkan.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.clblast.supported) {
|
||||
backends.push({
|
||||
value: 'clblast',
|
||||
label: 'CLBlast',
|
||||
devices: gpuCapabilities.clblast.devices,
|
||||
});
|
||||
}
|
||||
|
||||
backends.push({
|
||||
value: 'cpu',
|
||||
label: 'CPU',
|
||||
devices: cpuCapabilitiesResult.cpuInfo,
|
||||
});
|
||||
}
|
||||
|
||||
setAvailableBackends(backends);
|
||||
|
||||
if (
|
||||
backends.length > 0 &&
|
||||
(!backend || !backends.some((b) => b.value === backend))
|
||||
) {
|
||||
onBackendChange(backends[0].value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to detect available backends:', error);
|
||||
setAvailableBackends([]);
|
||||
} finally {
|
||||
setIsLoadingBackends(false);
|
||||
}
|
||||
};
|
||||
|
||||
void detectAvailableBackends();
|
||||
}, [backend, onBackendChange]);
|
||||
|
||||
const getWarnings = () => {
|
||||
if (backend !== 'cpu' || !cpuCapabilities) return [];
|
||||
|
||||
const warnings = [];
|
||||
|
||||
if (!cpuCapabilities.avx2 && !noavx2) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Your CPU does not support AVX2. Enable the "Disable AVX2" option to avoid crashes.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option to avoid crashes.',
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Backend
|
||||
</Text>
|
||||
<InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
|
||||
{getWarnings().map((warning, index) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
label={warning.message}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<ActionIcon size="sm" color="orange" variant="light">
|
||||
<AlertTriangle size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Group>
|
||||
<Select
|
||||
placeholder={
|
||||
isLoadingBackends
|
||||
? 'Detecting available backends...'
|
||||
: 'Select backend'
|
||||
}
|
||||
value={backend}
|
||||
onChange={(value) => value && onBackendChange(value)}
|
||||
data={availableBackends.map((b) => ({
|
||||
value: b.value,
|
||||
label: b.label,
|
||||
}))}
|
||||
disabled={isLoadingBackends || availableBackends.length === 0}
|
||||
/>
|
||||
{backend &&
|
||||
availableBackends.find((b) => b.value === backend)?.devices && (
|
||||
<Group gap="xs" mt="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
Devices:
|
||||
</Text>
|
||||
{availableBackends
|
||||
.find((b) => b.value === backend)
|
||||
?.devices?.map((device, index) => (
|
||||
<Badge key={index} variant="light" size="sm">
|
||||
{device}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{backend === 'cpu' && (
|
||||
<Card withBorder p="sm" mt="xs" bg={isDark ? 'blue.9' : 'blue.0'}>
|
||||
<Text size="sm" c={isDark ? 'blue.2' : 'blue.8'}>
|
||||
<Text component="span" fw={600}>
|
||||
Performance Note:
|
||||
</Text>{' '}
|
||||
LLMs run significantly faster on GPU-accelerated systems. Consider
|
||||
using NVIDIA's CUDA or AMD's ROCm backends for optimal
|
||||
performance.
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,14 +1,5 @@
|
|||
import {
|
||||
Stack,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
ActionIcon,
|
||||
Menu,
|
||||
Select,
|
||||
Badge,
|
||||
} from '@mantine/core';
|
||||
import { RotateCcw, Save, Settings2, File } from 'lucide-react';
|
||||
import { Stack, Text, Group, Button, Menu, Select, Badge } from '@mantine/core';
|
||||
import { Save, Settings2, File } from 'lucide-react';
|
||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
|
|
@ -16,7 +7,6 @@ interface ConfigurationManagerProps {
|
|||
configFiles: ConfigFile[];
|
||||
selectedFile: string | null;
|
||||
onFileSelection: (fileName: string) => void;
|
||||
onRefresh: () => void;
|
||||
onSaveAsNew: () => void;
|
||||
onUpdateCurrent: () => void;
|
||||
}
|
||||
|
|
@ -58,43 +48,31 @@ export const ConfigurationManager = ({
|
|||
configFiles,
|
||||
selectedFile,
|
||||
onFileSelection,
|
||||
onRefresh,
|
||||
onSaveAsNew,
|
||||
onUpdateCurrent,
|
||||
}: ConfigurationManagerProps) => (
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={500}>Configuration File</Text>
|
||||
<Group gap="xs">
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Button variant="light" leftSection={<Save size={16} />}>
|
||||
Save
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
|
||||
Save as new configuration
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<Settings2 size={16} />}
|
||||
disabled={!selectedFile}
|
||||
onClick={onUpdateCurrent}
|
||||
>
|
||||
Update current configuration
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
onClick={onRefresh}
|
||||
size="lg"
|
||||
aria-label="Refresh configuration files"
|
||||
title="Refresh configuration files"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Button variant="light" leftSection={<Save size={16} />}>
|
||||
Save
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
|
||||
Save as new configuration
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<Settings2 size={16} />}
|
||||
disabled={!selectedFile}
|
||||
onClick={onUpdateCurrent}
|
||||
>
|
||||
Update current configuration
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
|
||||
{configFiles.length === 0 ? (
|
||||
|
|
@ -6,16 +6,11 @@ import {
|
|||
Button,
|
||||
Checkbox,
|
||||
Slider,
|
||||
Select,
|
||||
Badge,
|
||||
Card,
|
||||
useMantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { File, Search } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { BackendSelector } from '@/screens/Launch/BackendSelector';
|
||||
import { getInputValidationState } from '@/utils/validation';
|
||||
import { isNoCudaBinary, isRocmBinary } from '@/utils/binaryUtils';
|
||||
|
||||
interface GeneralTabProps {
|
||||
modelPath: string;
|
||||
|
|
@ -23,6 +18,8 @@ interface GeneralTabProps {
|
|||
autoGpuLayers: boolean;
|
||||
contextSize: number;
|
||||
backend: string;
|
||||
noavx2: boolean;
|
||||
failsafe: boolean;
|
||||
onModelPathChange: (path: string) => void;
|
||||
onSelectModelFile: () => void;
|
||||
onGpuLayersChange: (layers: number) => void;
|
||||
|
|
@ -37,6 +34,8 @@ export const GeneralTab = ({
|
|||
autoGpuLayers,
|
||||
contextSize,
|
||||
backend,
|
||||
noavx2,
|
||||
failsafe,
|
||||
onModelPathChange,
|
||||
onSelectModelFile,
|
||||
onGpuLayersChange,
|
||||
|
|
@ -44,89 +43,6 @@ export const GeneralTab = ({
|
|||
onContextSizeChange,
|
||||
onBackendChange,
|
||||
}: GeneralTabProps) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
const [availableBackends, setAvailableBackends] = useState<
|
||||
Array<{ value: string; label: string; devices?: string[] }>
|
||||
>([]);
|
||||
const [isLoadingBackends, setIsLoadingBackends] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const detectAvailableBackends = async () => {
|
||||
setIsLoadingBackends(true);
|
||||
|
||||
try {
|
||||
const [currentBinaryInfo, _cpuCapabilities, gpuCapabilities] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentBinaryInfo(),
|
||||
window.electronAPI.kobold.detectCPU(),
|
||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||
]);
|
||||
|
||||
const backends: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
devices?: string[];
|
||||
}> = [];
|
||||
|
||||
if (currentBinaryInfo?.filename) {
|
||||
const filename = currentBinaryInfo.filename;
|
||||
|
||||
backends.push({ value: 'cpu', label: 'CPU' });
|
||||
|
||||
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) {
|
||||
backends.push({
|
||||
value: 'cuda',
|
||||
label: 'CUDA',
|
||||
devices: gpuCapabilities.cuda.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (isRocmBinary(filename) && gpuCapabilities.rocm.supported) {
|
||||
backends.push({
|
||||
value: 'rocm',
|
||||
label: 'ROCm',
|
||||
devices: gpuCapabilities.rocm.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.vulkan.supported) {
|
||||
backends.push({
|
||||
value: 'vulkan',
|
||||
label: 'Vulkan',
|
||||
devices: gpuCapabilities.vulkan.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.clblast.supported) {
|
||||
backends.push({
|
||||
value: 'clblast',
|
||||
label: 'CLBlast',
|
||||
devices: gpuCapabilities.clblast.devices,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setAvailableBackends(backends);
|
||||
|
||||
if (
|
||||
backends.length > 0 &&
|
||||
(!backend || !backends.some((b) => b.value === backend))
|
||||
) {
|
||||
onBackendChange(backends[0].value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to detect available backends:', error);
|
||||
setAvailableBackends([]);
|
||||
} finally {
|
||||
setIsLoadingBackends(false);
|
||||
}
|
||||
};
|
||||
|
||||
void detectAvailableBackends();
|
||||
}, [backend, onBackendChange]);
|
||||
|
||||
const validationState = getInputValidationState(modelPath);
|
||||
|
||||
const getInputColor = () => {
|
||||
|
|
@ -154,7 +70,7 @@ export const GeneralTab = ({
|
|||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
Text Model File *
|
||||
Text Model File
|
||||
</Text>
|
||||
<Group gap="xs" align="flex-start">
|
||||
<div style={{ flex: 1 }}>
|
||||
|
|
@ -162,7 +78,6 @@ export const GeneralTab = ({
|
|||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||
value={modelPath}
|
||||
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
||||
required
|
||||
color={getInputColor()}
|
||||
error={
|
||||
validationState === 'invalid' ? getHelperText() : undefined
|
||||
|
|
@ -190,55 +105,12 @@ export const GeneralTab = ({
|
|||
</Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Backend
|
||||
</Text>
|
||||
<InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
|
||||
</Group>
|
||||
<Select
|
||||
placeholder={
|
||||
isLoadingBackends
|
||||
? 'Detecting available backends...'
|
||||
: 'Select backend'
|
||||
}
|
||||
value={backend}
|
||||
onChange={(value) => value && onBackendChange(value)}
|
||||
data={availableBackends.map((b) => ({
|
||||
value: b.value,
|
||||
label: b.label,
|
||||
}))}
|
||||
disabled={isLoadingBackends || availableBackends.length === 0}
|
||||
/>
|
||||
{backend &&
|
||||
availableBackends.find((b) => b.value === backend)?.devices && (
|
||||
<Group gap="xs" mt="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
Devices:
|
||||
</Text>
|
||||
{availableBackends
|
||||
.find((b) => b.value === backend)
|
||||
?.devices?.map((device, index) => (
|
||||
<Badge key={index} variant="light" size="sm">
|
||||
{device}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{backend === 'cpu' && (
|
||||
<Card withBorder p="sm" mt="xs" bg={isDark ? 'blue.9' : 'blue.0'}>
|
||||
<Text size="sm" c={isDark ? 'blue.2' : 'blue.8'}>
|
||||
<Text component="span" fw={600}>
|
||||
Performance Note:
|
||||
</Text>{' '}
|
||||
LLMs run significantly faster on GPU-accelerated systems. Consider
|
||||
using NVIDIA's CUDA or AMD's ROCm backends for optimal
|
||||
performance.
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<BackendSelector
|
||||
backend={backend}
|
||||
onBackendChange={onBackendChange}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
233
src/screens/Launch/ImageGenerationTab.tsx
Normal file
233
src/screens/Launch/ImageGenerationTab.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import {
|
||||
Stack,
|
||||
Text,
|
||||
Group,
|
||||
TextInput,
|
||||
Button,
|
||||
Select,
|
||||
Alert,
|
||||
} from '@mantine/core';
|
||||
import { File, Search, AlertTriangle } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { getInputValidationState } from '@/utils/validation';
|
||||
import { IMAGE_MODEL_PRESETS } from '@/utils/imageModelPresets';
|
||||
|
||||
interface ImageGenerationTabProps {
|
||||
sdmodel: string;
|
||||
sdt5xxl: string;
|
||||
sdclipl: string;
|
||||
sdclipg: string;
|
||||
sdphotomaker: string;
|
||||
sdvae: string;
|
||||
textModelPath?: string;
|
||||
onSdmodelChange: (path: string) => void;
|
||||
onSelectSdmodelFile: () => void;
|
||||
onSdt5xxlChange: (path: string) => void;
|
||||
onSelectSdt5xxlFile: () => void;
|
||||
onSdcliplChange: (path: string) => void;
|
||||
onSelectSdcliplFile: () => void;
|
||||
onSdclipgChange: (path: string) => void;
|
||||
onSelectSdclipgFile: () => void;
|
||||
onSdphotomakerChange: (path: string) => void;
|
||||
onSelectSdphotomakerFile: () => void;
|
||||
onSdvaeChange: (path: string) => void;
|
||||
onSelectSdvaeFile: () => void;
|
||||
onApplyPreset: (presetName: string) => void;
|
||||
}
|
||||
|
||||
const ModelField = ({
|
||||
label,
|
||||
value,
|
||||
placeholder,
|
||||
tooltip,
|
||||
onChange,
|
||||
onSelectFile,
|
||||
showSearchHF = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
tooltip?: string;
|
||||
onChange: (value: string) => void;
|
||||
onSelectFile: () => void;
|
||||
showSearchHF?: boolean;
|
||||
}) => {
|
||||
const validationState = getInputValidationState(value);
|
||||
|
||||
const getInputColor = () => {
|
||||
switch (validationState) {
|
||||
case 'valid':
|
||||
return 'green';
|
||||
case 'invalid':
|
||||
return 'red';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getHelperText = () => {
|
||||
if (!value.trim()) return undefined;
|
||||
|
||||
if (validationState === 'invalid') {
|
||||
return 'Enter a valid URL or file path';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
{tooltip && <InfoTooltip label={tooltip} />}
|
||||
</Group>
|
||||
<Group gap="xs" align="flex-start">
|
||||
<div style={{ flex: 1 }}>
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.currentTarget.value)}
|
||||
color={getInputColor()}
|
||||
error={validationState === 'invalid' ? getHelperText() : undefined}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onSelectFile}
|
||||
variant="light"
|
||||
leftSection={<File size={16} />}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
{showSearchHF && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.electronAPI.app.openExternal(
|
||||
'https://huggingface.co/models?pipeline_tag=text-to-image&library=gguf&sort=trending'
|
||||
);
|
||||
}}
|
||||
variant="outline"
|
||||
leftSection={<Search size={16} />}
|
||||
>
|
||||
Search HF
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageGenerationTab = ({
|
||||
sdmodel,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
textModelPath,
|
||||
onSdmodelChange,
|
||||
onSelectSdmodelFile,
|
||||
onSdt5xxlChange,
|
||||
onSelectSdt5xxlFile,
|
||||
onSdcliplChange,
|
||||
onSelectSdcliplFile,
|
||||
onSdclipgChange,
|
||||
onSelectSdclipgFile,
|
||||
onSdphotomakerChange,
|
||||
onSelectSdphotomakerFile,
|
||||
onSdvaeChange,
|
||||
onSelectSdvaeFile,
|
||||
onApplyPreset,
|
||||
}: ImageGenerationTabProps) => {
|
||||
const hasTextModel = textModelPath?.trim() !== '';
|
||||
const hasImageModel = sdmodel.trim() !== '';
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Model Preset
|
||||
</Text>
|
||||
<InfoTooltip label="Quick presets for popular image generation models with pre-configured encoders." />
|
||||
</Group>
|
||||
<Select
|
||||
placeholder="Choose a preset..."
|
||||
data={IMAGE_MODEL_PRESETS.map((preset) => ({
|
||||
value: preset.name,
|
||||
label: preset.name,
|
||||
}))}
|
||||
value={null}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
onApplyPreset(value);
|
||||
}
|
||||
}}
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasTextModel && hasImageModel && (
|
||||
<Alert
|
||||
color="orange"
|
||||
title="Model Priority Notice"
|
||||
icon={<AlertTriangle size={16} />}
|
||||
>
|
||||
Both text and image generation models are selected. The image
|
||||
generation model will take priority and be used for launch.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ModelField
|
||||
label="Image Gen. Model File"
|
||||
value={sdmodel}
|
||||
placeholder="Select a model file or enter a direct URL"
|
||||
onChange={onSdmodelChange}
|
||||
onSelectFile={onSelectSdmodelFile}
|
||||
showSearchHF
|
||||
/>
|
||||
|
||||
<ModelField
|
||||
label="T5-XXL File"
|
||||
value={sdt5xxl}
|
||||
placeholder="Select a T5-XXL file or enter a direct URL"
|
||||
onChange={onSdt5xxlChange}
|
||||
onSelectFile={onSelectSdt5xxlFile}
|
||||
/>
|
||||
|
||||
<ModelField
|
||||
label="Clip-L File"
|
||||
value={sdclipl}
|
||||
placeholder="Select a Clip-L file or enter a direct URL"
|
||||
onChange={onSdcliplChange}
|
||||
onSelectFile={onSelectSdcliplFile}
|
||||
/>
|
||||
|
||||
<ModelField
|
||||
label="Clip-G File"
|
||||
value={sdclipg}
|
||||
placeholder="Select a Clip-G file or enter a direct URL"
|
||||
onChange={onSdclipgChange}
|
||||
onSelectFile={onSelectSdclipgFile}
|
||||
/>
|
||||
|
||||
<ModelField
|
||||
label="PhotoMaker"
|
||||
value={sdphotomaker}
|
||||
placeholder="Select a PhotoMaker file or enter a direct URL"
|
||||
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
|
||||
onChange={onSdphotomakerChange}
|
||||
onSelectFile={onSelectSdphotomakerFile}
|
||||
/>
|
||||
|
||||
<ModelField
|
||||
label="Image VAE"
|
||||
value={sdvae}
|
||||
placeholder="Select a VAE file or enter a direct URL"
|
||||
onChange={onSdvaeChange}
|
||||
onSelectFile={onSelectSdvaeFile}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
@ -10,18 +10,23 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { ConfigurationManager } from '@/components/launch/ConfigurationManager';
|
||||
import { GeneralTab } from '@/components/launch/GeneralTab';
|
||||
import { AdvancedTab } from '@/components/launch/AdvancedTab';
|
||||
import { NetworkTab } from '@/components/launch/NetworkTab';
|
||||
import { SaveConfigModal } from '@/components/launch/SaveConfigModal';
|
||||
import { ConfigurationManager } from '@/screens/Launch/ConfigurationManager';
|
||||
import { GeneralTab } from '@/screens/Launch/GeneralTab';
|
||||
import { AdvancedTab } from '@/screens/Launch/AdvancedTab';
|
||||
import { NetworkTab } from '@/screens/Launch/NetworkTab';
|
||||
import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab';
|
||||
import { SaveConfigModal } from '@/screens/Launch/SaveConfigModal';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface LaunchScreenProps {
|
||||
onLaunch: () => void;
|
||||
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||
}
|
||||
|
||||
export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||
export const LaunchScreen = ({
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
}: LaunchScreenProps) => {
|
||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [, setInstallDir] = useState<string>('');
|
||||
|
|
@ -49,6 +54,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
noavx2,
|
||||
failsafe,
|
||||
backend,
|
||||
sdmodel,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
parseAndApplyConfigFile,
|
||||
loadSavedSettings,
|
||||
loadConfigFromFile,
|
||||
|
|
@ -71,6 +82,19 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleBackendChange,
|
||||
handleSdmodelChange,
|
||||
handleSelectSdmodelFile,
|
||||
handleSdt5xxlChange,
|
||||
handleSelectSdt5xxlFile,
|
||||
handleSdcliplChange,
|
||||
handleSelectSdcliplFile,
|
||||
handleSdclipgChange,
|
||||
handleSelectSdclipgFile,
|
||||
handleSdphotomakerChange,
|
||||
handleSelectSdphotomakerFile,
|
||||
handleSdvaeChange,
|
||||
handleSelectSdvaeFile,
|
||||
handleApplyPreset,
|
||||
} = useLaunchConfig();
|
||||
|
||||
const loadConfigFiles = useCallback(async () => {
|
||||
|
|
@ -199,6 +223,36 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSdmodelChangeWithTracking = (path: string) => {
|
||||
handleSdmodelChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSdt5xxlChangeWithTracking = (path: string) => {
|
||||
handleSdt5xxlChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSdcliplChangeWithTracking = (path: string) => {
|
||||
handleSdcliplChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSdclipgChangeWithTracking = (path: string) => {
|
||||
handleSdclipgChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSdphotomakerChangeWithTracking = (path: string) => {
|
||||
handleSdphotomakerChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSdvaeChangeWithTracking = (path: string) => {
|
||||
handleSdvaeChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfigFiles();
|
||||
|
||||
|
|
@ -213,8 +267,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
return cleanup;
|
||||
}, [loadConfigFiles]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleLaunch = async () => {
|
||||
if (isLaunching || !modelPath) {
|
||||
const isImageMode = sdmodel.trim() !== '';
|
||||
const isTextMode = modelPath.trim() !== '';
|
||||
|
||||
if (isLaunching || (!isImageMode && !isTextMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +285,33 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
|
||||
const args: string[] = [];
|
||||
|
||||
args.push('--model', modelPath);
|
||||
if (isImageMode && isTextMode) {
|
||||
args.push('--sdmodel', sdmodel);
|
||||
} else if (isImageMode) {
|
||||
args.push('--sdmodel', sdmodel);
|
||||
|
||||
if (sdt5xxl.trim()) {
|
||||
args.push('--sdt5xxl', sdt5xxl);
|
||||
}
|
||||
|
||||
if (sdclipl.trim()) {
|
||||
args.push('--sdclipl', sdclipl);
|
||||
}
|
||||
|
||||
if (sdclipg.trim()) {
|
||||
args.push('--sdclipg', sdclipg);
|
||||
}
|
||||
|
||||
if (sdphotomaker.trim()) {
|
||||
args.push('--sdphotomaker', sdphotomaker);
|
||||
}
|
||||
|
||||
if (sdvae.trim()) {
|
||||
args.push('--sdvae', sdvae);
|
||||
}
|
||||
} else {
|
||||
args.push('--model', modelPath);
|
||||
}
|
||||
|
||||
if (autoGpuLayers) {
|
||||
args.push('--gpulayers', '-1');
|
||||
|
|
@ -298,6 +382,10 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
);
|
||||
|
||||
if (result.success) {
|
||||
if (onLaunchModeChange) {
|
||||
onLaunchModeChange(isImageMode);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
onLaunch();
|
||||
}, 100);
|
||||
|
|
@ -332,7 +420,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
</div>
|
||||
<Button
|
||||
radius="md"
|
||||
disabled={!modelPath || isLaunching}
|
||||
disabled={(!modelPath && !sdmodel) || isLaunching}
|
||||
onClick={handleLaunch}
|
||||
loading={isLaunching}
|
||||
size="lg"
|
||||
|
|
@ -371,6 +459,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
autoGpuLayers={autoGpuLayers}
|
||||
contextSize={contextSize}
|
||||
backend={backend}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
onModelPathChange={handleModelPathChangeWithTracking}
|
||||
onSelectModelFile={handleSelectModelFile}
|
||||
onGpuLayersChange={handleGpuLayersChangeWithTracking}
|
||||
|
|
@ -421,12 +511,28 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="image" pt="md">
|
||||
<Stack gap="lg" align="center" py="xl">
|
||||
<Text c="dimmed" ta="center">
|
||||
Image generation configuration will be available in a future
|
||||
update.
|
||||
</Text>
|
||||
</Stack>
|
||||
<ImageGenerationTab
|
||||
sdmodel={sdmodel}
|
||||
sdt5xxl={sdt5xxl}
|
||||
sdclipl={sdclipl}
|
||||
sdclipg={sdclipg}
|
||||
sdphotomaker={sdphotomaker}
|
||||
sdvae={sdvae}
|
||||
textModelPath={modelPath}
|
||||
onSdmodelChange={handleSdmodelChangeWithTracking}
|
||||
onSelectSdmodelFile={handleSelectSdmodelFile}
|
||||
onSdt5xxlChange={handleSdt5xxlChangeWithTracking}
|
||||
onSelectSdt5xxlFile={handleSelectSdt5xxlFile}
|
||||
onSdcliplChange={handleSdcliplChangeWithTracking}
|
||||
onSelectSdcliplFile={handleSelectSdcliplFile}
|
||||
onSdclipgChange={handleSdclipgChangeWithTracking}
|
||||
onSelectSdclipgFile={handleSelectSdclipgFile}
|
||||
onSdphotomakerChange={handleSdphotomakerChangeWithTracking}
|
||||
onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
|
||||
onSdvaeChange={handleSdvaeChangeWithTracking}
|
||||
onSelectSdvaeFile={handleSelectSdvaeFile}
|
||||
onApplyPreset={handleApplyPreset}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</div>
|
||||
</Tabs>
|
||||
29
src/types/electron.d.ts
vendored
29
src/types/electron.d.ts
vendored
|
|
@ -93,6 +93,35 @@ export interface KoboldAPI {
|
|||
getConfigFiles: () => Promise<
|
||||
Array<{ name: string; path: string; size: number }>
|
||||
>;
|
||||
saveConfigFile: (
|
||||
configName: string,
|
||||
configData: {
|
||||
gpulayers?: number;
|
||||
contextsize?: number;
|
||||
model_param?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
multiuser?: number;
|
||||
multiplayer?: boolean;
|
||||
remotetunnel?: boolean;
|
||||
nocertify?: boolean;
|
||||
websearch?: boolean;
|
||||
noshift?: boolean;
|
||||
flashattention?: boolean;
|
||||
noavx2?: boolean;
|
||||
failsafe?: boolean;
|
||||
usecuda?: boolean;
|
||||
usevulkan?: boolean;
|
||||
useclblast?: boolean;
|
||||
sdmodel?: string;
|
||||
sdt5xxl?: string;
|
||||
sdclipl?: string;
|
||||
sdclipg?: string;
|
||||
sdphotomaker?: string;
|
||||
sdvae?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
) => Promise<boolean>;
|
||||
getSelectedConfig: () => Promise<string | null>;
|
||||
setSelectedConfig: (configName: string) => Promise<boolean>;
|
||||
parseConfigFile: (filePath: string) => Promise<{
|
||||
|
|
|
|||
54
src/utils/imageModelPresets.ts
Normal file
54
src/utils/imageModelPresets.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
export interface ImageModelPreset {
|
||||
name: string;
|
||||
description?: string;
|
||||
sdmodel: string;
|
||||
sdt5xxl: string;
|
||||
sdclipl: string;
|
||||
sdclipg: string;
|
||||
sdphotomaker: string;
|
||||
sdvae: string;
|
||||
}
|
||||
|
||||
export const IMAGE_MODEL_PRESETS: ImageModelPreset[] = [
|
||||
{
|
||||
name: 'FLUX.1',
|
||||
description: 'FLUX.1 development model with default encoders',
|
||||
sdmodel:
|
||||
'https://huggingface.co/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf?download=true',
|
||||
sdt5xxl:
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true',
|
||||
sdclipl:
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true',
|
||||
sdclipg: '',
|
||||
sdphotomaker: '',
|
||||
sdvae:
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true',
|
||||
},
|
||||
{
|
||||
name: 'Chroma',
|
||||
description: 'Chroma with optimized VAE and shared encoders',
|
||||
sdmodel:
|
||||
'https://huggingface.co/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v29/chroma-unlocked-v29-Q4_K_S.gguf?download=true',
|
||||
sdt5xxl:
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true',
|
||||
sdclipl:
|
||||
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true',
|
||||
sdclipg: '',
|
||||
sdphotomaker: '',
|
||||
sdvae:
|
||||
'https://huggingface.co/lodestones/Chroma/resolve/main/ae.safetensors?download=true',
|
||||
},
|
||||
{
|
||||
name: 'Custom',
|
||||
description: 'Start with empty fields for custom configuration',
|
||||
sdmodel: '',
|
||||
sdt5xxl: '',
|
||||
sdclipl: '',
|
||||
sdclipg: '',
|
||||
sdphotomaker: '',
|
||||
sdvae: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const getPresetByName = (name: string): ImageModelPreset | undefined =>
|
||||
IMAGE_MODEL_PRESETS.find((preset) => preset.name === name);
|
||||
Loading…
Add table
Reference in a new issue