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
|
# 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
|
### General Coding
|
||||||
|
|
||||||
- Follow existing TypeScript/React patterns in the codebase
|
- Follow existing TypeScript/React patterns in the codebase
|
||||||
|
|
@ -18,3 +9,7 @@
|
||||||
- Never create tests, docs or github workflows
|
- Never create tests, docs or github workflows
|
||||||
- Stop asking me to run the "dev" script to test changes
|
- 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
|
- 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
|
- 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
|
- automatically unpack all downloaded koboldcpp binaries for significantly faster operation and reduced RAM+HDD utilization (up to ~4GB less RAM usage for ROCm)
|
||||||
|
|
||||||
### Prerequisites
|
### 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).
|
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
|
### 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.
|
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",
|
"kobold",
|
||||||
"KOBOLDAI",
|
"KOBOLDAI",
|
||||||
"koboldcpp",
|
"koboldcpp",
|
||||||
|
"sdmodel",
|
||||||
|
"sdt5xxl",
|
||||||
|
"sdclipl",
|
||||||
|
"sdclipg",
|
||||||
|
"sdphotomaker",
|
||||||
|
"sdvae",
|
||||||
|
"sdapi",
|
||||||
"less",
|
"less",
|
||||||
"letterspacing",
|
"letterspacing",
|
||||||
"libvk",
|
"libvk",
|
||||||
|
|
@ -183,6 +190,8 @@
|
||||||
"subdomain",
|
"subdomain",
|
||||||
"svg",
|
"svg",
|
||||||
"swiftshader",
|
"swiftshader",
|
||||||
|
"safetensors",
|
||||||
|
"SDXL",
|
||||||
"tabler",
|
"tabler",
|
||||||
"Tauri",
|
"Tauri",
|
||||||
"temp",
|
"temp",
|
||||||
|
|
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -11,7 +11,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/core": "^8.2.4",
|
"@mantine/core": "^8.2.4",
|
||||||
"jiti": "^2.5.1",
|
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|
@ -20,7 +19,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cspell/eslint-plugin": "^9.2.0",
|
"@cspell/eslint-plugin": "^9.2.0",
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.3.0",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@types/systeminformation": "^3.54.1",
|
"@types/systeminformation": "^3.54.1",
|
||||||
|
|
@ -41,6 +40,7 @@
|
||||||
"eslint-plugin-sonarjs": "^3.0.4",
|
"eslint-plugin-sonarjs": "^3.0.4",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"jiti": "^2.5.1",
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
|
|
@ -3227,9 +3227,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.2.1",
|
"version": "24.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -8393,6 +8393,7 @@
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cspell/eslint-plugin": "^9.2.0",
|
"@cspell/eslint-plugin": "^9.2.0",
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.3.0",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@types/systeminformation": "^3.54.1",
|
"@types/systeminformation": "^3.54.1",
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
"eslint-plugin-sonarjs": "^3.0.4",
|
"eslint-plugin-sonarjs": "^3.0.4",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"jiti": "^2.5.1",
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
|
|
@ -79,7 +80,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/core": "^8.2.4",
|
"@mantine/core": "^8.2.4",
|
||||||
"jiti": "^2.5.1",
|
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
|
|
||||||
22
src/App.tsx
22
src/App.tsx
|
|
@ -3,7 +3,6 @@ import {
|
||||||
AppShell,
|
AppShell,
|
||||||
Group,
|
Group,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
|
||||||
rem,
|
rem,
|
||||||
Loader,
|
Loader,
|
||||||
Center,
|
Center,
|
||||||
|
|
@ -15,12 +14,13 @@ import {
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Settings, ArrowLeft } from 'lucide-react';
|
import { Settings, ArrowLeft } from 'lucide-react';
|
||||||
import { DownloadScreen } from '@/components/screens/DownloadScreen';
|
import { DownloadScreen } from '@/screens/Download';
|
||||||
import { LaunchScreen } from '@/components/screens/LaunchScreen';
|
import { LaunchScreen } from '@/screens/Launch';
|
||||||
import { InterfaceScreen } from '@/components/screens/InterfaceScreen';
|
import { InterfaceScreen } from '@/screens/Interface';
|
||||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||||
import { SettingsModal } from '@/components/SettingsModal';
|
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||||
|
import { StyledTooltip } from '@/components/StyledTooltip';
|
||||||
import type { UpdateInfo, InstalledVersion } from '@/types';
|
import type { UpdateInfo, InstalledVersion } from '@/types';
|
||||||
|
|
||||||
type Screen = 'download' | 'launch' | 'interface';
|
type Screen = 'download' | 'launch' | 'interface';
|
||||||
|
|
@ -34,6 +34,8 @@ export const App = () => {
|
||||||
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
|
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
|
||||||
'terminal'
|
'terminal'
|
||||||
);
|
);
|
||||||
|
const [isImageGenerationMode, setIsImageGenerationMode] =
|
||||||
|
useState<boolean>(false);
|
||||||
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
@ -316,7 +318,7 @@ export const App = () => {
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip label="Settings" position="bottom">
|
<StyledTooltip label="Settings" position="bottom">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
|
|
@ -329,7 +331,7 @@ export const App = () => {
|
||||||
>
|
>
|
||||||
<Settings style={{ width: rem(20), height: rem(20) }} />
|
<Settings style={{ width: rem(20), height: rem(20) }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</StyledTooltip>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
@ -362,7 +364,10 @@ export const App = () => {
|
||||||
isActive={currentScreen === 'launch'}
|
isActive={currentScreen === 'launch'}
|
||||||
shouldAnimate={hasInitialized}
|
shouldAnimate={hasInitialized}
|
||||||
>
|
>
|
||||||
<LaunchScreen onLaunch={handleLaunch} />
|
<LaunchScreen
|
||||||
|
onLaunch={handleLaunch}
|
||||||
|
onLaunchModeChange={setIsImageGenerationMode}
|
||||||
|
/>
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
|
|
||||||
<ScreenTransition
|
<ScreenTransition
|
||||||
|
|
@ -372,6 +377,7 @@ export const App = () => {
|
||||||
<InterfaceScreen
|
<InterfaceScreen
|
||||||
activeTab={activeInterfaceTab}
|
activeTab={activeInterfaceTab}
|
||||||
onTabChange={setActiveInterfaceTab}
|
onTabChange={setActiveInterfaceTab}
|
||||||
|
isImageGenerationMode={isImageGenerationMode}
|
||||||
/>
|
/>
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ActionIcon, Tooltip, useMantineColorScheme } from '@mantine/core';
|
import { ActionIcon } from '@mantine/core';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
import { StyledTooltip } from './StyledTooltip';
|
||||||
|
|
||||||
interface InfoTooltipProps {
|
interface InfoTooltipProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -11,34 +12,10 @@ export const InfoTooltip = ({
|
||||||
label,
|
label,
|
||||||
multiline = true,
|
multiline = true,
|
||||||
width = 300,
|
width = 300,
|
||||||
}: InfoTooltipProps) => {
|
}: InfoTooltipProps) => (
|
||||||
const { colorScheme } = useMantineColorScheme();
|
<StyledTooltip label={label} multiline={multiline} w={width}>
|
||||||
const isDark = colorScheme === 'dark';
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
return (
|
</ActionIcon>
|
||||||
<Tooltip
|
</StyledTooltip>
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
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 { useState, useEffect } from 'react';
|
||||||
import { Modal, Tabs, Text, Group, rem } from '@mantine/core';
|
import { Modal, Tabs, Text, Group, rem } from '@mantine/core';
|
||||||
import { Settings, Palette, SlidersHorizontal, GitBranch } from 'lucide-react';
|
import { Settings, Palette, SlidersHorizontal, GitBranch } from 'lucide-react';
|
||||||
import { GeneralTab } from './settings/GeneralTab';
|
import { GeneralTab } from './GeneralTab';
|
||||||
import { VersionsTab } from './settings/VersionsTab';
|
import { VersionsTab } from './VersionsTab';
|
||||||
import { AppearanceTab } from './settings/AppearanceTab';
|
import { AppearanceTab } from './AppearanceTab';
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -28,6 +28,8 @@ export const SettingsModal = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened) {
|
if (opened) {
|
||||||
|
setActiveTab('general');
|
||||||
|
|
||||||
const originalOverflow = document.body.style.overflow;
|
const originalOverflow = document.body.style.overflow;
|
||||||
const scrollbarWidth =
|
const scrollbarWidth =
|
||||||
window.innerWidth - document.documentElement.clientWidth;
|
window.innerWidth - document.documentElement.clientWidth;
|
||||||
|
|
@ -11,13 +11,13 @@ import {
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { RotateCcw } from 'lucide-react';
|
import { RotateCcw } from 'lucide-react';
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import { isAssetCompatibleWithPlatform } from '@/utils/platform';
|
import {
|
||||||
import { getAssetDescription } from '@/utils/assets';
|
getAssetDescription,
|
||||||
import type {
|
sortAssetsByRecommendation,
|
||||||
InstalledVersion,
|
isAssetRecommended,
|
||||||
GitHubAsset,
|
} from '@/utils/assets';
|
||||||
GitHubRelease,
|
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||||
} from '@/types/electron';
|
import type { InstalledVersion } from '@/types/electron';
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -31,30 +31,26 @@ interface VersionInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VersionsTab = () => {
|
export const VersionsTab = () => {
|
||||||
|
const {
|
||||||
|
platformInfo,
|
||||||
|
latestRelease,
|
||||||
|
filteredAssets,
|
||||||
|
rocmDownload,
|
||||||
|
loadingPlatform,
|
||||||
|
loadingRemote,
|
||||||
|
downloading,
|
||||||
|
downloadProgress,
|
||||||
|
loadRemoteVersions,
|
||||||
|
handleDownload: sharedHandleDownload,
|
||||||
|
} = useKoboldVersions();
|
||||||
|
|
||||||
const [installedVersions, setInstalledVersions] = useState<
|
const [installedVersions, setInstalledVersions] = useState<
|
||||||
InstalledVersion[]
|
InstalledVersion[]
|
||||||
>([]);
|
>([]);
|
||||||
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
|
||||||
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 [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 () => {
|
const loadInstalledVersions = useCallback(async () => {
|
||||||
setLoadingInstalled(true);
|
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(() => {
|
useEffect(() => {
|
||||||
loadInstalledVersions();
|
loadInstalledVersions();
|
||||||
if (userPlatform) {
|
}, [loadInstalledVersions]);
|
||||||
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]);
|
|
||||||
|
|
||||||
const getDisplayNameFromPath = (
|
const getDisplayNameFromPath = (
|
||||||
installedVersion: InstalledVersion
|
installedVersion: InstalledVersion
|
||||||
|
|
@ -181,7 +118,7 @@ export const VersionsTab = () => {
|
||||||
const getAllVersions = (): VersionInfo[] => {
|
const getAllVersions = (): VersionInfo[] => {
|
||||||
const versions: VersionInfo[] = [];
|
const versions: VersionInfo[] = [];
|
||||||
|
|
||||||
availableAssets.forEach((asset) => {
|
filteredAssets.forEach((asset) => {
|
||||||
const installedVersion = installedVersions.find((v) => {
|
const installedVersion = installedVersions.find((v) => {
|
||||||
const displayName = getDisplayNameFromPath(v);
|
const displayName = getDisplayNameFromPath(v);
|
||||||
return displayName === asset.name;
|
return displayName === asset.name;
|
||||||
|
|
@ -260,38 +197,28 @@ export const VersionsTab = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return versions;
|
return sortAssetsByRecommendation(versions, platformInfo.hasAMDGPU);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (version: VersionInfo) => {
|
const handleDownload = async (version: VersionInfo) => {
|
||||||
setDownloading(version.name);
|
|
||||||
setDownloadProgress((prev) => ({ ...prev, [version.name]: 0 }));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result;
|
let success;
|
||||||
|
|
||||||
if (version.isROCm) {
|
if (version.isROCm) {
|
||||||
result = await window.electronAPI.kobold.downloadROCm();
|
success = await sharedHandleDownload('rocm');
|
||||||
} else {
|
} else {
|
||||||
const asset = availableAssets.find((a) => a.name === version.name);
|
const asset = filteredAssets.find((a) => a.name === version.name);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new Error('Asset not found');
|
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();
|
await loadInstalledVersions();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to download:', 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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -329,7 +256,7 @@ export const VersionsTab = () => {
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Loader size="lg" />
|
<Loader size="lg" />
|
||||||
<Text c="dimmed">
|
<Text c="dimmed">
|
||||||
{loadingInstalled && loadingRemote
|
{loadingInstalled && (loadingPlatform || loadingRemote)
|
||||||
? 'Loading versions...'
|
? 'Loading versions...'
|
||||||
: loadingInstalled
|
: loadingInstalled
|
||||||
? 'Scanning installed versions...'
|
? 'Scanning installed versions...'
|
||||||
|
|
@ -374,6 +301,10 @@ export const VersionsTab = () => {
|
||||||
}
|
}
|
||||||
version={version.version}
|
version={version.version}
|
||||||
description={getAssetDescription(version.name)}
|
description={getAssetDescription(version.name)}
|
||||||
|
isRecommended={isAssetRecommended(
|
||||||
|
version.name,
|
||||||
|
platformInfo.hasAMDGPU
|
||||||
|
)}
|
||||||
isCurrent={version.isCurrent}
|
isCurrent={version.isCurrent}
|
||||||
isInstalled={version.isInstalled}
|
isInstalled={version.isInstalled}
|
||||||
isDownloading={isDownloading}
|
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 { useState, useCallback } from 'react';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
|
import {
|
||||||
|
getPresetByName,
|
||||||
|
type ImageModelPreset,
|
||||||
|
} from '@/utils/imageModelPresets';
|
||||||
|
|
||||||
export const useLaunchConfig = () => {
|
export const useLaunchConfig = () => {
|
||||||
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
||||||
|
|
@ -21,10 +25,24 @@ export const useLaunchConfig = () => {
|
||||||
const [failsafe, setFailsafe] = useState<boolean>(false);
|
const [failsafe, setFailsafe] = useState<boolean>(false);
|
||||||
const [backend, setBackend] = useState<string>('cpu');
|
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
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||||
const configData =
|
const configData =
|
||||||
await window.electronAPI.kobold.parseConfigFile(configPath);
|
await window.electronAPI.kobold.parseConfigFile(configPath);
|
||||||
|
|
||||||
if (configData) {
|
if (configData) {
|
||||||
if (typeof configData.gpulayers === 'number') {
|
if (typeof configData.gpulayers === 'number') {
|
||||||
setGpuLayers(configData.gpulayers);
|
setGpuLayers(configData.gpulayers);
|
||||||
|
|
@ -124,7 +142,32 @@ export const useLaunchConfig = () => {
|
||||||
} else {
|
} else {
|
||||||
setBackend('cpu');
|
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 {
|
} else {
|
||||||
|
const cpuCapabilities = await window.electronAPI.kobold.detectCPU();
|
||||||
setGpuLayers(0);
|
setGpuLayers(0);
|
||||||
setContextSize(2048);
|
setContextSize(2048);
|
||||||
setPort(5001);
|
setPort(5001);
|
||||||
|
|
@ -136,14 +179,30 @@ export const useLaunchConfig = () => {
|
||||||
setWebsearch(false);
|
setWebsearch(false);
|
||||||
setNoshift(false);
|
setNoshift(false);
|
||||||
setFlashattention(false);
|
setFlashattention(false);
|
||||||
setNoavx2(false);
|
setNoavx2(!cpuCapabilities.avx2);
|
||||||
setFailsafe(false);
|
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
|
||||||
setBackend('cpu');
|
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 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);
|
setServerOnly(savedServerOnly);
|
||||||
setModelPath('');
|
setModelPath('');
|
||||||
|
|
@ -158,9 +217,22 @@ export const useLaunchConfig = () => {
|
||||||
setWebsearch(false);
|
setWebsearch(false);
|
||||||
setNoshift(false);
|
setNoshift(false);
|
||||||
setFlashattention(false);
|
setFlashattention(false);
|
||||||
setNoavx2(false);
|
setNoavx2(!cpuCapabilities.avx2);
|
||||||
setFailsafe(false);
|
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
|
||||||
setBackend('cpu');
|
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(
|
const loadConfigFromFile = useCallback(
|
||||||
|
|
@ -278,6 +350,91 @@ export const useLaunchConfig = () => {
|
||||||
setBackend(backend);
|
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 {
|
return {
|
||||||
serverOnly,
|
serverOnly,
|
||||||
gpuLayers,
|
gpuLayers,
|
||||||
|
|
@ -297,6 +454,12 @@ export const useLaunchConfig = () => {
|
||||||
noavx2,
|
noavx2,
|
||||||
failsafe,
|
failsafe,
|
||||||
backend,
|
backend,
|
||||||
|
sdmodel,
|
||||||
|
sdt5xxl,
|
||||||
|
sdclipl,
|
||||||
|
sdclipg,
|
||||||
|
sdphotomaker,
|
||||||
|
sdvae,
|
||||||
|
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
|
|
@ -320,5 +483,19 @@ export const useLaunchConfig = () => {
|
||||||
handleNoavx2Change,
|
handleNoavx2Change,
|
||||||
handleFailsafeChange,
|
handleFailsafeChange,
|
||||||
handleBackendChange,
|
handleBackendChange,
|
||||||
|
handleSdmodelChange,
|
||||||
|
handleSelectSdmodelFile,
|
||||||
|
handleSdt5xxlChange,
|
||||||
|
handleSelectSdt5xxlFile,
|
||||||
|
handleSdcliplChange,
|
||||||
|
handleSelectSdcliplFile,
|
||||||
|
handleSdclipgChange,
|
||||||
|
handleSelectSdclipgFile,
|
||||||
|
handleSdphotomakerChange,
|
||||||
|
handleSelectSdphotomakerFile,
|
||||||
|
handleSdvaeChange,
|
||||||
|
handleSelectSdvaeFile,
|
||||||
|
applyImageModelPreset,
|
||||||
|
handleApplyPreset,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
createWriteStream,
|
createWriteStream,
|
||||||
chmodSync,
|
chmodSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
import { dialog } from 'electron';
|
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> {
|
async selectModelFile(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable no-comments/disallowComments */
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import type {
|
import type {
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
|
|
@ -7,7 +8,15 @@ import type {
|
||||||
} from '@/types/hardware';
|
} from '@/types/hardware';
|
||||||
|
|
||||||
export class HardwareService {
|
export class HardwareService {
|
||||||
|
private cpuCapabilitiesCache: CPUCapabilities | null = null;
|
||||||
|
private basicGPUInfoCache: BasicGPUInfo | null = null;
|
||||||
|
private gpuCapabilitiesCache: GPUCapabilities | null = null;
|
||||||
|
|
||||||
async detectCPU(): Promise<CPUCapabilities> {
|
async detectCPU(): Promise<CPUCapabilities> {
|
||||||
|
if (this.cpuCapabilitiesCache) {
|
||||||
|
return this.cpuCapabilitiesCache;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
|
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 avx = flags.includes('avx') || flags.includes('AVX');
|
||||||
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
||||||
|
|
||||||
return {
|
this.cpuCapabilitiesCache = {
|
||||||
avx,
|
avx,
|
||||||
avx2,
|
avx2,
|
||||||
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'],
|
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return this.cpuCapabilitiesCache;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('CPU detection failed:', error);
|
console.warn('CPU detection failed:', error);
|
||||||
return {
|
const fallbackCapabilities = {
|
||||||
avx: false,
|
avx: false,
|
||||||
avx2: false,
|
avx2: false,
|
||||||
cpuInfo: ['CPU detection failed'],
|
cpuInfo: ['CPU detection failed'],
|
||||||
};
|
};
|
||||||
|
this.cpuCapabilitiesCache = fallbackCapabilities;
|
||||||
|
return fallbackCapabilities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectGPU(): Promise<BasicGPUInfo> {
|
async detectGPU(): Promise<BasicGPUInfo> {
|
||||||
|
if (this.basicGPUInfoCache) {
|
||||||
|
return this.basicGPUInfoCache;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const graphics = await si.graphics();
|
const graphics = await si.graphics();
|
||||||
|
|
||||||
|
|
@ -76,23 +93,33 @@ export class HardwareService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
this.basicGPUInfoCache = {
|
||||||
hasAMD,
|
hasAMD,
|
||||||
hasNVIDIA,
|
hasNVIDIA,
|
||||||
gpuInfo:
|
gpuInfo:
|
||||||
gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return this.basicGPUInfoCache;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('GPU detection failed:', error);
|
console.warn('GPU detection failed:', error);
|
||||||
return {
|
const fallbackGPUInfo = {
|
||||||
hasAMD: false,
|
hasAMD: false,
|
||||||
hasNVIDIA: false,
|
hasNVIDIA: false,
|
||||||
gpuInfo: ['GPU detection failed'],
|
gpuInfo: ['GPU detection failed'],
|
||||||
};
|
};
|
||||||
|
this.basicGPUInfoCache = fallbackGPUInfo;
|
||||||
|
return fallbackGPUInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectGPUCapabilities(): Promise<GPUCapabilities> {
|
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([
|
const [cuda, rocm, vulkan, clblast] = await Promise.all([
|
||||||
this.detectCUDA(),
|
this.detectCUDA(),
|
||||||
this.detectROCm(),
|
this.detectROCm(),
|
||||||
|
|
@ -100,7 +127,8 @@ export class HardwareService {
|
||||||
this.detectCLBlast(),
|
this.detectCLBlast(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { cuda, rocm, vulkan, clblast };
|
this.gpuCapabilitiesCache = { cuda, rocm, vulkan, clblast };
|
||||||
|
return this.gpuCapabilitiesCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectAll(): Promise<HardwareInfo> {
|
async detectAll(): Promise<HardwareInfo> {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,12 @@ export class IPCHandlers {
|
||||||
this.koboldManager.getConfigFiles()
|
this.koboldManager.getConfigFiles()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
'kobold:saveConfigFile',
|
||||||
|
async (_event, configName, configData) =>
|
||||||
|
this.koboldManager.saveConfigFile(configName, configData)
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getSelectedConfig', () =>
|
ipcMain.handle('kobold:getSelectedConfig', () =>
|
||||||
this.configManager.getSelectedConfig()
|
this.configManager.getSelectedConfig()
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,35 @@ const koboldAPI: KoboldAPI = {
|
||||||
openInstallDialog: () => ipcRenderer.invoke('kobold:openInstallDialog'),
|
openInstallDialog: () => ipcRenderer.invoke('kobold:openInstallDialog'),
|
||||||
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
|
||||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
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'),
|
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
||||||
setSelectedConfig: (configName: string) =>
|
setSelectedConfig: (configName: string) =>
|
||||||
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -7,13 +7,10 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Container,
|
Container,
|
||||||
Badge,
|
Badge,
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import {
|
import { StyledTooltip } from '@/components/StyledTooltip';
|
||||||
getPlatformDisplayName,
|
import { getPlatformDisplayName } from '@/utils/platform';
|
||||||
filterAssetsByPlatform,
|
|
||||||
} from '@/utils/platform';
|
|
||||||
import { formatFileSize } from '@/utils/fileSize';
|
import { formatFileSize } from '@/utils/fileSize';
|
||||||
import {
|
import {
|
||||||
isAssetRecommended,
|
isAssetRecommended,
|
||||||
|
|
@ -21,127 +18,59 @@ import {
|
||||||
getAssetDescription,
|
getAssetDescription,
|
||||||
} from '@/utils/assets';
|
} from '@/utils/assets';
|
||||||
import { ROCM } from '@/constants';
|
import { ROCM } from '@/constants';
|
||||||
import type { GitHubAsset, GitHubRelease } from '@/types';
|
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||||
|
import type { GitHubAsset } from '@/types';
|
||||||
|
|
||||||
interface DownloadScreenProps {
|
interface DownloadScreenProps {
|
||||||
onDownloadComplete: () => void;
|
onDownloadComplete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
const {
|
||||||
null
|
platformInfo,
|
||||||
);
|
latestRelease,
|
||||||
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
|
filteredAssets,
|
||||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
rocmDownload,
|
||||||
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
|
loadingPlatform,
|
||||||
const [hasROCm, setHasROCm] = useState<boolean>(false);
|
loadingRemote,
|
||||||
const [loading, setLoading] = useState(true);
|
downloading,
|
||||||
const [downloading, setDownloading] = useState(false);
|
downloadProgress,
|
||||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
handleDownload: sharedHandleDownload,
|
||||||
|
} = useKoboldVersions();
|
||||||
|
|
||||||
const [downloadingType, setDownloadingType] = useState<
|
const [downloadingType, setDownloadingType] = useState<
|
||||||
'asset' | 'rocm' | null
|
'asset' | 'rocm' | null
|
||||||
>(null);
|
>(null);
|
||||||
const [downloadingAsset, setDownloadingAsset] = useState<string | 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 () => {
|
const loading = loadingPlatform || loadingRemote;
|
||||||
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 handleDownload = useCallback(
|
const handleDownload = useCallback(
|
||||||
async (type: 'asset' | 'rocm', asset?: GitHubAsset) => {
|
async (type: 'asset' | 'rocm', asset?: GitHubAsset) => {
|
||||||
if (type === 'asset' && !asset) return;
|
if (type === 'asset' && !asset) return;
|
||||||
|
|
||||||
setDownloading(true);
|
|
||||||
setDownloadingType(type);
|
setDownloadingType(type);
|
||||||
setDownloadingAsset(type === 'asset' ? asset!.name : null);
|
setDownloadingAsset(type === 'asset' ? asset!.name : null);
|
||||||
setDownloadProgress(0);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (type === 'rocm'
|
const success = await sharedHandleDownload(type, asset);
|
||||||
? window.electronAPI.kobold.downloadROCm()
|
|
||||||
: await window.electronAPI.kobold.downloadRelease(asset!));
|
|
||||||
|
|
||||||
onDownloadComplete();
|
if (success) {
|
||||||
|
onDownloadComplete();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDownloading(false);
|
setDownloadingType(null);
|
||||||
setDownloadingType(null);
|
setDownloadingAsset(null);
|
||||||
setDownloadingAsset(null);
|
}, 200);
|
||||||
setDownloadProgress(0);
|
}
|
||||||
}, 200);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to download ${type}:`, error);
|
console.error(`Failed to download ${type}:`, error);
|
||||||
setDownloading(false);
|
} finally {
|
||||||
setDownloadingType(null);
|
setDownloadingType(null);
|
||||||
setDownloadingAsset(null);
|
setDownloadingAsset(null);
|
||||||
setDownloadProgress(0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onDownloadComplete]
|
[sharedHandleDownload, onDownloadComplete]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderROCmCard = () => {
|
const renderROCmCard = () => {
|
||||||
|
|
@ -152,10 +81,17 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
name={ROCM.BINARY_NAME}
|
name={ROCM.BINARY_NAME}
|
||||||
size={formatFileSize(ROCM.SIZE_BYTES)}
|
size={formatFileSize(ROCM.SIZE_BYTES)}
|
||||||
description={getAssetDescription(ROCM.BINARY_NAME)}
|
description={getAssetDescription(ROCM.BINARY_NAME)}
|
||||||
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
|
isRecommended={isAssetRecommended(
|
||||||
isDownloading={downloading && downloadingType === 'rocm'}
|
ROCM.BINARY_NAME,
|
||||||
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
|
platformInfo.hasAMDGPU
|
||||||
disabled={downloading && downloadingType !== 'rocm'}
|
)}
|
||||||
|
isDownloading={Boolean(downloading) && downloadingType === 'rocm'}
|
||||||
|
downloadProgress={
|
||||||
|
downloadingType === 'rocm'
|
||||||
|
? downloadProgress[ROCM.BINARY_NAME] || 0
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
disabled={Boolean(downloading) && downloadingType !== 'rocm'}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload('rocm');
|
handleDownload('rocm');
|
||||||
|
|
@ -208,7 +144,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
|
|
||||||
{filteredAssets.length > 0 || rocmDownload ? (
|
{filteredAssets.length > 0 || rocmDownload ? (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{hasAMDGPU && !hasROCm && (
|
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
|
||||||
<Card withBorder p="md" bg="orange.0">
|
<Card withBorder p="md" bg="orange.0">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<div
|
<div
|
||||||
|
|
@ -221,7 +157,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
<Text fw={600} c="orange.9">
|
<Text fw={600} c="orange.9">
|
||||||
AMD GPU Detected
|
AMD GPU Detected
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
<StyledTooltip
|
||||||
label="ROCm is not installed. Install ROCm for optimal AMD GPU performance with KoboldCpp."
|
label="ROCm is not installed. Install ROCm for optimal AMD GPU performance with KoboldCpp."
|
||||||
multiline
|
multiline
|
||||||
w={220}
|
w={220}
|
||||||
|
|
@ -233,7 +169,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
>
|
>
|
||||||
ROCm Not Found
|
ROCm Not Found
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</StyledTooltip>
|
||||||
</div>
|
</div>
|
||||||
<Text size="sm" c="orange.8">
|
<Text size="sm" c="orange.8">
|
||||||
For best performance with your AMD GPU, consider
|
For best performance with your AMD GPU, consider
|
||||||
|
|
@ -243,11 +179,13 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{rocmDownload && hasAMDGPU && renderROCmCard()}
|
{rocmDownload &&
|
||||||
|
platformInfo.hasAMDGPU &&
|
||||||
|
renderROCmCard()}
|
||||||
|
|
||||||
{sortAssetsByRecommendation(
|
{sortAssetsByRecommendation(
|
||||||
filteredAssets,
|
filteredAssets,
|
||||||
hasAMDGPU
|
platformInfo.hasAMDGPU
|
||||||
).map((asset) => (
|
).map((asset) => (
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
key={asset.name}
|
key={asset.name}
|
||||||
|
|
@ -256,22 +194,23 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
description={getAssetDescription(asset.name)}
|
description={getAssetDescription(asset.name)}
|
||||||
isRecommended={isAssetRecommended(
|
isRecommended={isAssetRecommended(
|
||||||
asset.name,
|
asset.name,
|
||||||
hasAMDGPU
|
platformInfo.hasAMDGPU
|
||||||
)}
|
)}
|
||||||
isDownloading={
|
isDownloading={
|
||||||
downloading &&
|
Boolean(downloading) &&
|
||||||
downloadingType === 'asset' &&
|
downloadingType === 'asset' &&
|
||||||
downloadingAsset === asset.name
|
downloadingAsset === asset.name
|
||||||
}
|
}
|
||||||
downloadProgress={
|
downloadProgress={
|
||||||
downloading &&
|
Boolean(downloading) &&
|
||||||
downloadingType === 'asset' &&
|
downloadingType === 'asset' &&
|
||||||
downloadingAsset === asset.name
|
downloadingAsset === asset.name
|
||||||
? downloadProgress
|
? downloadProgress[asset.name] || 0
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
downloading && downloadingAsset !== asset.name
|
Boolean(downloading) &&
|
||||||
|
downloadingAsset !== asset.name
|
||||||
}
|
}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -280,7 +219,9 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{rocmDownload && !hasAMDGPU && renderROCmCard()}
|
{rocmDownload &&
|
||||||
|
!platformInfo.hasAMDGPU &&
|
||||||
|
renderROCmCard()}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Card withBorder p="md" bg="yellow.0" c="yellow.9">
|
<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 fw={500}>No downloads available</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
No downloads available for your platform (
|
No downloads available for your platform (
|
||||||
{getPlatformDisplayName(userPlatform)}).
|
{getPlatformDisplayName(platformInfo.platform)}).
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Box, Text, Stack } from '@mantine/core';
|
import { Box, Text, Stack } from '@mantine/core';
|
||||||
|
|
||||||
interface ChatTabProps {
|
interface ImageGenerationTabProps {
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
isServerReady?: boolean;
|
isServerReady?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatTab = ({ serverUrl, isServerReady }: ChatTabProps) => {
|
export const ImageGenerationTab = ({
|
||||||
|
serverUrl,
|
||||||
|
isServerReady,
|
||||||
|
}: ImageGenerationTabProps) => {
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
if (!isServerReady || !serverUrl) {
|
if (!isServerReady || !serverUrl) {
|
||||||
|
|
@ -25,19 +28,21 @@ export const ChatTab = ({ serverUrl, isServerReady }: ChatTabProps) => {
|
||||||
Waiting for KoboldCpp server to start...
|
Waiting for KoboldCpp server to start...
|
||||||
</Text>
|
</Text>
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
The chat interface will load automatically when ready
|
The image generation interface will load automatically when ready
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageGenerationUrl = `${serverUrl}/sdapi/v1`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
src={serverUrl}
|
src={imageGenerationUrl}
|
||||||
title="KoboldAI Lite Interface"
|
title="KoboldCpp Image Generation Interface"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '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 { useState, useEffect, useCallback } from 'react';
|
||||||
import { Card, Container } from '@mantine/core';
|
import { Card, Container } from '@mantine/core';
|
||||||
import { ChatTab } from '@/components/interface/ChatTab';
|
import { ServerTab } from '@/screens/Interface/ServerTab';
|
||||||
import { TerminalTab } from '@/components/interface/TerminalTab';
|
import { TerminalTab } from '@/screens/Interface/TerminalTab';
|
||||||
|
|
||||||
interface InterfaceScreenProps {
|
interface InterfaceScreenProps {
|
||||||
activeTab?: string | null;
|
activeTab?: string | null;
|
||||||
onTabChange?: (tab: string | null) => void;
|
onTabChange?: (tab: string | null) => void;
|
||||||
|
isImageGenerationMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InterfaceScreen = ({
|
export const InterfaceScreen = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
|
isImageGenerationMode = false,
|
||||||
}: InterfaceScreenProps) => {
|
}: InterfaceScreenProps) => {
|
||||||
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
||||||
const [serverUrl, setServerUrl] = useState<string>('');
|
const [serverUrl, setServerUrl] = useState<string>('');
|
||||||
|
|
@ -22,10 +24,10 @@ export const InterfaceScreen = ({
|
||||||
setIsServerReady(true);
|
setIsServerReady(true);
|
||||||
|
|
||||||
if (!serverOnly && onTabChange) {
|
if (!serverOnly && onTabChange) {
|
||||||
onTabChange('chat');
|
onTabChange(isImageGenerationMode ? 'image' : 'chat');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[serverOnly, onTabChange]
|
[serverOnly, onTabChange, isImageGenerationMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -66,7 +68,23 @@ export const InterfaceScreen = ({
|
||||||
display: activeTab === 'chat' ? 'block' : 'none',
|
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>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -31,22 +31,6 @@ export const AdvancedTab = ({
|
||||||
onFailsafeChange,
|
onFailsafeChange,
|
||||||
}: AdvancedTabProps) => (
|
}: AdvancedTabProps) => (
|
||||||
<Stack gap="lg">
|
<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>
|
<div>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
|
|
@ -115,5 +99,20 @@ export const AdvancedTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</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>
|
</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 {
|
import { Stack, Text, Group, Button, Menu, Select, Badge } from '@mantine/core';
|
||||||
Stack,
|
import { Save, Settings2, File } from 'lucide-react';
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
Button,
|
|
||||||
ActionIcon,
|
|
||||||
Menu,
|
|
||||||
Select,
|
|
||||||
Badge,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { RotateCcw, Save, Settings2, File } from 'lucide-react';
|
|
||||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
|
|
@ -16,7 +7,6 @@ interface ConfigurationManagerProps {
|
||||||
configFiles: ConfigFile[];
|
configFiles: ConfigFile[];
|
||||||
selectedFile: string | null;
|
selectedFile: string | null;
|
||||||
onFileSelection: (fileName: string) => void;
|
onFileSelection: (fileName: string) => void;
|
||||||
onRefresh: () => void;
|
|
||||||
onSaveAsNew: () => void;
|
onSaveAsNew: () => void;
|
||||||
onUpdateCurrent: () => void;
|
onUpdateCurrent: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -58,43 +48,31 @@ export const ConfigurationManager = ({
|
||||||
configFiles,
|
configFiles,
|
||||||
selectedFile,
|
selectedFile,
|
||||||
onFileSelection,
|
onFileSelection,
|
||||||
onRefresh,
|
|
||||||
onSaveAsNew,
|
onSaveAsNew,
|
||||||
onUpdateCurrent,
|
onUpdateCurrent,
|
||||||
}: ConfigurationManagerProps) => (
|
}: ConfigurationManagerProps) => (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Text fw={500}>Configuration File</Text>
|
<Text fw={500}>Configuration File</Text>
|
||||||
<Group gap="xs">
|
<Menu>
|
||||||
<Menu>
|
<Menu.Target>
|
||||||
<Menu.Target>
|
<Button variant="light" leftSection={<Save size={16} />}>
|
||||||
<Button variant="light" leftSection={<Save size={16} />}>
|
Save
|
||||||
Save
|
</Button>
|
||||||
</Button>
|
</Menu.Target>
|
||||||
</Menu.Target>
|
<Menu.Dropdown>
|
||||||
<Menu.Dropdown>
|
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
|
||||||
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
|
Save as new configuration
|
||||||
Save as new configuration
|
</Menu.Item>
|
||||||
</Menu.Item>
|
<Menu.Item
|
||||||
<Menu.Item
|
leftSection={<Settings2 size={16} />}
|
||||||
leftSection={<Settings2 size={16} />}
|
disabled={!selectedFile}
|
||||||
disabled={!selectedFile}
|
onClick={onUpdateCurrent}
|
||||||
onClick={onUpdateCurrent}
|
>
|
||||||
>
|
Update current configuration
|
||||||
Update current configuration
|
</Menu.Item>
|
||||||
</Menu.Item>
|
</Menu.Dropdown>
|
||||||
</Menu.Dropdown>
|
</Menu>
|
||||||
</Menu>
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
onClick={onRefresh}
|
|
||||||
size="lg"
|
|
||||||
aria-label="Refresh configuration files"
|
|
||||||
title="Refresh configuration files"
|
|
||||||
>
|
|
||||||
<RotateCcw size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{configFiles.length === 0 ? (
|
{configFiles.length === 0 ? (
|
||||||
|
|
@ -6,16 +6,11 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Slider,
|
Slider,
|
||||||
Select,
|
|
||||||
Badge,
|
|
||||||
Card,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
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 { BackendSelector } from '@/screens/Launch/BackendSelector';
|
||||||
import { getInputValidationState } from '@/utils/validation';
|
import { getInputValidationState } from '@/utils/validation';
|
||||||
import { isNoCudaBinary, isRocmBinary } from '@/utils/binaryUtils';
|
|
||||||
|
|
||||||
interface GeneralTabProps {
|
interface GeneralTabProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
|
|
@ -23,6 +18,8 @@ interface GeneralTabProps {
|
||||||
autoGpuLayers: boolean;
|
autoGpuLayers: boolean;
|
||||||
contextSize: number;
|
contextSize: number;
|
||||||
backend: string;
|
backend: string;
|
||||||
|
noavx2: boolean;
|
||||||
|
failsafe: boolean;
|
||||||
onModelPathChange: (path: string) => void;
|
onModelPathChange: (path: string) => void;
|
||||||
onSelectModelFile: () => void;
|
onSelectModelFile: () => void;
|
||||||
onGpuLayersChange: (layers: number) => void;
|
onGpuLayersChange: (layers: number) => void;
|
||||||
|
|
@ -37,6 +34,8 @@ export const GeneralTab = ({
|
||||||
autoGpuLayers,
|
autoGpuLayers,
|
||||||
contextSize,
|
contextSize,
|
||||||
backend,
|
backend,
|
||||||
|
noavx2,
|
||||||
|
failsafe,
|
||||||
onModelPathChange,
|
onModelPathChange,
|
||||||
onSelectModelFile,
|
onSelectModelFile,
|
||||||
onGpuLayersChange,
|
onGpuLayersChange,
|
||||||
|
|
@ -44,89 +43,6 @@ export const GeneralTab = ({
|
||||||
onContextSizeChange,
|
onContextSizeChange,
|
||||||
onBackendChange,
|
onBackendChange,
|
||||||
}: GeneralTabProps) => {
|
}: 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 validationState = getInputValidationState(modelPath);
|
||||||
|
|
||||||
const getInputColor = () => {
|
const getInputColor = () => {
|
||||||
|
|
@ -154,7 +70,7 @@ export const GeneralTab = ({
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm" fw={500} mb="xs">
|
<Text size="sm" fw={500} mb="xs">
|
||||||
Text Model File *
|
Text Model File
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs" align="flex-start">
|
<Group gap="xs" align="flex-start">
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
|
|
@ -162,7 +78,6 @@ export const GeneralTab = ({
|
||||||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||||
value={modelPath}
|
value={modelPath}
|
||||||
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
||||||
required
|
|
||||||
color={getInputColor()}
|
color={getInputColor()}
|
||||||
error={
|
error={
|
||||||
validationState === 'invalid' ? getHelperText() : undefined
|
validationState === 'invalid' ? getHelperText() : undefined
|
||||||
|
|
@ -190,55 +105,12 @@ export const GeneralTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<BackendSelector
|
||||||
<Group gap="xs" align="center" mb="xs">
|
backend={backend}
|
||||||
<Text size="sm" fw={500}>
|
onBackendChange={onBackendChange}
|
||||||
Backend
|
noavx2={noavx2}
|
||||||
</Text>
|
failsafe={failsafe}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Group justify="space-between" align="center" mb="xs">
|
<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';
|
} from '@mantine/core';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
import { ConfigurationManager } from '@/components/launch/ConfigurationManager';
|
import { ConfigurationManager } from '@/screens/Launch/ConfigurationManager';
|
||||||
import { GeneralTab } from '@/components/launch/GeneralTab';
|
import { GeneralTab } from '@/screens/Launch/GeneralTab';
|
||||||
import { AdvancedTab } from '@/components/launch/AdvancedTab';
|
import { AdvancedTab } from '@/screens/Launch/AdvancedTab';
|
||||||
import { NetworkTab } from '@/components/launch/NetworkTab';
|
import { NetworkTab } from '@/screens/Launch/NetworkTab';
|
||||||
import { SaveConfigModal } from '@/components/launch/SaveConfigModal';
|
import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab';
|
||||||
|
import { SaveConfigModal } from '@/screens/Launch/SaveConfigModal';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
interface LaunchScreenProps {
|
interface LaunchScreenProps {
|
||||||
onLaunch: () => void;
|
onLaunch: () => void;
|
||||||
|
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
export const LaunchScreen = ({
|
||||||
|
onLaunch,
|
||||||
|
onLaunchModeChange,
|
||||||
|
}: LaunchScreenProps) => {
|
||||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||||
const [, setInstallDir] = useState<string>('');
|
const [, setInstallDir] = useState<string>('');
|
||||||
|
|
@ -49,6 +54,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
noavx2,
|
noavx2,
|
||||||
failsafe,
|
failsafe,
|
||||||
backend,
|
backend,
|
||||||
|
sdmodel,
|
||||||
|
sdt5xxl,
|
||||||
|
sdclipl,
|
||||||
|
sdclipg,
|
||||||
|
sdphotomaker,
|
||||||
|
sdvae,
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
loadConfigFromFile,
|
loadConfigFromFile,
|
||||||
|
|
@ -71,6 +82,19 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
handleNoavx2Change,
|
handleNoavx2Change,
|
||||||
handleFailsafeChange,
|
handleFailsafeChange,
|
||||||
handleBackendChange,
|
handleBackendChange,
|
||||||
|
handleSdmodelChange,
|
||||||
|
handleSelectSdmodelFile,
|
||||||
|
handleSdt5xxlChange,
|
||||||
|
handleSelectSdt5xxlFile,
|
||||||
|
handleSdcliplChange,
|
||||||
|
handleSelectSdcliplFile,
|
||||||
|
handleSdclipgChange,
|
||||||
|
handleSelectSdclipgFile,
|
||||||
|
handleSdphotomakerChange,
|
||||||
|
handleSelectSdphotomakerFile,
|
||||||
|
handleSdvaeChange,
|
||||||
|
handleSelectSdvaeFile,
|
||||||
|
handleApplyPreset,
|
||||||
} = useLaunchConfig();
|
} = useLaunchConfig();
|
||||||
|
|
||||||
const loadConfigFiles = useCallback(async () => {
|
const loadConfigFiles = useCallback(async () => {
|
||||||
|
|
@ -199,6 +223,36 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
setHasUnsavedChanges(true);
|
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(() => {
|
useEffect(() => {
|
||||||
void loadConfigFiles();
|
void loadConfigFiles();
|
||||||
|
|
||||||
|
|
@ -213,8 +267,12 @@ 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) {
|
const isImageMode = sdmodel.trim() !== '';
|
||||||
|
const isTextMode = modelPath.trim() !== '';
|
||||||
|
|
||||||
|
if (isLaunching || (!isImageMode && !isTextMode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +285,33 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
|
|
||||||
const args: string[] = [];
|
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) {
|
if (autoGpuLayers) {
|
||||||
args.push('--gpulayers', '-1');
|
args.push('--gpulayers', '-1');
|
||||||
|
|
@ -298,6 +382,10 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
if (onLaunchModeChange) {
|
||||||
|
onLaunchModeChange(isImageMode);
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onLaunch();
|
onLaunch();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
@ -332,7 +420,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
radius="md"
|
radius="md"
|
||||||
disabled={!modelPath || isLaunching}
|
disabled={(!modelPath && !sdmodel) || isLaunching}
|
||||||
onClick={handleLaunch}
|
onClick={handleLaunch}
|
||||||
loading={isLaunching}
|
loading={isLaunching}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|
@ -371,6 +459,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
autoGpuLayers={autoGpuLayers}
|
autoGpuLayers={autoGpuLayers}
|
||||||
contextSize={contextSize}
|
contextSize={contextSize}
|
||||||
backend={backend}
|
backend={backend}
|
||||||
|
noavx2={noavx2}
|
||||||
|
failsafe={failsafe}
|
||||||
onModelPathChange={handleModelPathChangeWithTracking}
|
onModelPathChange={handleModelPathChangeWithTracking}
|
||||||
onSelectModelFile={handleSelectModelFile}
|
onSelectModelFile={handleSelectModelFile}
|
||||||
onGpuLayersChange={handleGpuLayersChangeWithTracking}
|
onGpuLayersChange={handleGpuLayersChangeWithTracking}
|
||||||
|
|
@ -421,12 +511,28 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="image" pt="md">
|
<Tabs.Panel value="image" pt="md">
|
||||||
<Stack gap="lg" align="center" py="xl">
|
<ImageGenerationTab
|
||||||
<Text c="dimmed" ta="center">
|
sdmodel={sdmodel}
|
||||||
Image generation configuration will be available in a future
|
sdt5xxl={sdt5xxl}
|
||||||
update.
|
sdclipl={sdclipl}
|
||||||
</Text>
|
sdclipg={sdclipg}
|
||||||
</Stack>
|
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>
|
</Tabs.Panel>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
29
src/types/electron.d.ts
vendored
29
src/types/electron.d.ts
vendored
|
|
@ -93,6 +93,35 @@ export interface KoboldAPI {
|
||||||
getConfigFiles: () => Promise<
|
getConfigFiles: () => Promise<
|
||||||
Array<{ name: string; path: string; size: number }>
|
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>;
|
getSelectedConfig: () => Promise<string | null>;
|
||||||
setSelectedConfig: (configName: string) => Promise<boolean>;
|
setSelectedConfig: (configName: string) => Promise<boolean>;
|
||||||
parseConfigFile: (filePath: string) => Promise<{
|
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