fixing clblast, more refactoring of slop code, zustand for complex tab state logic

This commit is contained in:
Egor 2025-08-20 17:11:56 -07:00
parent ed4fa55478
commit 1c5bd49e3c
23 changed files with 1779 additions and 1386 deletions

View file

@ -33,6 +33,7 @@
"KOBOLDAI",
"koboldcpp",
"KoboldCpp",
"lora",
"lowvram",
"Lowvram",
"maximizable",
@ -50,6 +51,7 @@
"nvidia",
"oldpc",
"OLDPC",
"opencl",
"Papi",
"Philippov",
"pkexec",

View file

@ -82,9 +82,11 @@
"@mantine/core": "^8.2.5",
"@mantine/hooks": "^8.2.5",
"lucide-react": "^0.540.0",
"opencl-info": "^0.3.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"systeminformation": "^5.27.7"
"systeminformation": "^5.27.7",
"zustand": "^5.0.8"
},
"build": {
"appId": "com.friendly-kobold.app",

View file

@ -21,7 +21,6 @@ import styles from '@/styles/layout.module.css';
interface ConfigFileManagerProps {
configFiles: ConfigFile[];
selectedFile: string | null;
hasUnsavedChanges: boolean;
onFileSelection: (fileName: string) => Promise<void>;
onCreateNewConfig: (configName: string) => Promise<void>;
onSaveConfig: () => void;
@ -64,7 +63,6 @@ SelectItem.displayName = 'SelectItem';
export const ConfigFileManager = ({
configFiles,
selectedFile,
hasUnsavedChanges,
onFileSelection,
onCreateNewConfig,
onSaveConfig,
@ -108,15 +106,6 @@ export const ConfigFileManager = ({
};
});
if (selectedFile === null && hasUnsavedChanges) {
const displayName = 'New Configuration (unsaved)';
selectData.unshift({
value: '__new__',
label: displayName,
extension: '.kcpps',
});
}
return (
<>
<Stack gap="xs">
@ -127,11 +116,7 @@ export const ConfigFileManager = ({
<div className={styles.flex1}>
<Select
placeholder="Select a configuration file"
value={
selectedFile === null && hasUnsavedChanges
? '__new__'
: selectedFile
}
value={selectedFile}
onChange={(value: string | null) => {
if (value === '__new__') {
return;
@ -156,24 +141,18 @@ export const ConfigFileManager = ({
/>
</div>
<Button
variant={
selectedFile === null && hasUnsavedChanges ? 'filled' : 'light'
}
variant="light"
leftSection={<Plus size={14} />}
size="sm"
disabled={selectedFile === null && hasUnsavedChanges}
onClick={() => handleOpenConfigModal()}
>
{selectedFile === null && hasUnsavedChanges
? 'Creating New...'
: 'New'}
New
</Button>
<Button
variant="outline"
leftSection={<Save size={14} />}
size="sm"
disabled={!hasUnsavedChanges}
onClick={() => {
if (selectedFile) {
onSaveConfig();

View file

@ -1,27 +1,11 @@
import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core';
import { Stack, Checkbox, Group, Text, TextInput } from '@mantine/core';
import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css';
interface AdvancedTabProps {
additionalArguments: string;
noshift: boolean;
flashattention: boolean;
noavx2: boolean;
failsafe: boolean;
lowvram: boolean;
quantmatmul: boolean;
backend: string;
onAdditionalArgumentsChange: (args: string) => void;
onNoshiftChange: (noshift: boolean) => void;
onFlashattentionChange: (flashattention: boolean) => void;
onNoavx2Change: (noavx2: boolean) => void;
onFailsafeChange: (failsafe: boolean) => void;
onLowvramChange: (lowvram: boolean) => void;
onQuantmatmulChange: (quantmatmul: boolean) => void;
}
export const AdvancedTab = ({
export const AdvancedTab = () => {
const {
additionalArguments,
noshift,
flashattention,
@ -30,14 +14,14 @@ export const AdvancedTab = ({
lowvram,
quantmatmul,
backend,
onAdditionalArgumentsChange,
onNoshiftChange,
onFlashattentionChange,
onNoavx2Change,
onFailsafeChange,
onLowvramChange,
onQuantmatmulChange,
}: AdvancedTabProps) => {
handleAdditionalArgumentsChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
} = useLaunchConfig();
const [backendSupport, setBackendSupport] = useState<{
noavx2: boolean;
failsafe: boolean;
@ -87,7 +71,7 @@ export const AdvancedTab = ({
<Checkbox
checked={!noshift}
onChange={(event) =>
onNoshiftChange(!event.currentTarget.checked)
handleNoshiftChange(!event.currentTarget.checked)
}
label="Context Shift"
/>
@ -100,7 +84,7 @@ export const AdvancedTab = ({
<Checkbox
checked={noshift}
onChange={(event) =>
onNoshiftChange(event.currentTarget.checked)
handleNoshiftChange(event.currentTarget.checked)
}
label="No Shift"
/>
@ -115,7 +99,7 @@ export const AdvancedTab = ({
<Checkbox
checked={flashattention}
onChange={(event) =>
onFlashattentionChange(event.currentTarget.checked)
handleFlashattentionChange(event.currentTarget.checked)
}
label="Flash Attention"
/>
@ -128,7 +112,7 @@ export const AdvancedTab = ({
<Checkbox
checked={lowvram}
onChange={(event) =>
onLowvramChange(event.currentTarget.checked)
handleLowvramChange(event.currentTarget.checked)
}
label="Low VRAM"
disabled={backend !== 'cuda' && backend !== 'rocm'}
@ -150,7 +134,7 @@ export const AdvancedTab = ({
<Checkbox
checked={quantmatmul}
onChange={(event) =>
onQuantmatmulChange(event.currentTarget.checked)
handleQuantmatmulChange(event.currentTarget.checked)
}
label="QuantMatMul"
disabled={backend !== 'cuda' && backend !== 'rocm'}
@ -181,7 +165,7 @@ export const AdvancedTab = ({
<Checkbox
checked={noavx2}
onChange={(event) =>
onNoavx2Change(event.currentTarget.checked)
handleNoavx2Change(event.currentTarget.checked)
}
label="Disable AVX2"
disabled={isLoading || !backendSupport?.noavx2}
@ -201,7 +185,7 @@ export const AdvancedTab = ({
<Checkbox
checked={failsafe}
onChange={(event) =>
onFailsafeChange(event.currentTarget.checked)
handleFailsafeChange(event.currentTarget.checked)
}
label="Failsafe"
disabled={isLoading || !backendSupport?.failsafe}
@ -230,7 +214,7 @@ export const AdvancedTab = ({
placeholder="Additional command line arguments"
value={additionalArguments}
onChange={(event) =>
onAdditionalArgumentsChange(event.currentTarget.value)
handleAdditionalArgumentsChange(event.currentTarget.value)
}
/>
</div>

View file

@ -1,157 +0,0 @@
import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
import { getInputValidationState } from '@/utils';
import styles from '@/styles/layout.module.css';
interface GeneralTabProps {
modelPath: string;
gpuLayers: number;
autoGpuLayers: boolean;
contextSize: number;
backend: string;
gpuDevice?: number;
noavx2: boolean;
failsafe: boolean;
onModelPathChange: (path: string) => void;
onSelectModelFile: () => void;
onGpuLayersChange: (layers: number) => void;
onAutoGpuLayersChange: (auto: boolean) => void;
onContextSizeChange: (size: number) => void;
onBackendChange: (backend: string) => void;
onGpuDeviceChange?: (device: number) => void;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
}
export const GeneralTab = ({
modelPath,
gpuLayers,
autoGpuLayers,
contextSize,
backend,
gpuDevice,
noavx2,
failsafe,
onModelPathChange,
onSelectModelFile,
onGpuLayersChange,
onAutoGpuLayersChange,
onContextSizeChange,
onBackendChange,
onGpuDeviceChange,
onWarningsChange,
onBackendsReady,
}: GeneralTabProps) => {
const validationState = getInputValidationState(modelPath);
const getInputColor = () => {
switch (validationState) {
case 'valid':
return 'green';
case 'invalid':
return 'red';
default:
return undefined;
}
};
const getHelperText = () => {
if (!modelPath.trim()) return undefined;
if (validationState === 'invalid') {
return 'Enter a valid URL or file path to the .gguf';
}
return undefined;
};
return (
<Stack gap="md">
<BackendSelector
backend={backend}
onBackendChange={onBackendChange}
gpuDevice={gpuDevice}
onGpuDeviceChange={onGpuDeviceChange}
noavx2={noavx2}
failsafe={failsafe}
onWarningsChange={onWarningsChange}
onBackendsReady={onBackendsReady}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
onGpuLayersChange={onGpuLayersChange}
onAutoGpuLayersChange={onAutoGpuLayersChange}
/>
<div>
<Text size="sm" fw={500} mb="xs">
Text Model File
</Text>
<Group gap="xs" align="flex-start">
<div className={styles.flex1}>
<TextInput
placeholder="Select a .gguf model file or enter a direct URL to file"
value={modelPath}
onChange={(event) => onModelPathChange(event.currentTarget.value)}
color={getInputColor()}
error={
validationState === 'invalid' ? getHelperText() : undefined
}
/>
</div>
<Button
onClick={onSelectModelFile}
variant="light"
leftSection={<File size={16} />}
>
Browse
</Button>
<Button
onClick={() => {
window.electronAPI.app.openExternal(
'https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending'
);
}}
variant="outline"
leftSection={<Search size={16} />}
>
Search HF
</Button>
</Group>
</div>
<div>
<Group justify="space-between" align="center" mb="xs">
<Group gap="xs" align="center">
<Text size="sm" fw={500}>
Context Size
</Text>
<InfoTooltip label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory." />
</Group>
<TextInput
value={contextSize?.toString() || ''}
onChange={(event) =>
onContextSizeChange(Number(event.target.value) || 256)
}
type="number"
min={256}
max={131072}
step={256}
size="sm"
w={100}
/>
</Group>
<Slider
value={contextSize}
min={256}
max={131072}
step={1}
onChange={onContextSizeChange}
/>
</div>
</Stack>
);
};

View file

@ -2,38 +2,32 @@ import { Text, Group, Select, Checkbox, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
interface BackendSelectorProps {
backend: string;
onBackendChange: (backend: string) => void;
gpuDevice?: number;
onGpuDeviceChange?: (device: number) => void;
noavx2?: boolean;
failsafe?: boolean;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
gpuLayers?: number;
autoGpuLayers?: boolean;
onGpuLayersChange?: (layers: number) => void;
onAutoGpuLayersChange?: (auto: boolean) => void;
}
export const BackendSelector = ({
backend,
onBackendChange,
gpuDevice = 0,
onGpuDeviceChange,
noavx2 = false,
failsafe = false,
onWarningsChange,
onBackendsReady,
gpuLayers = 0,
autoGpuLayers = false,
onGpuLayersChange,
onAutoGpuLayersChange,
}: BackendSelectorProps) => {
const {
backend,
gpuDevice,
noavx2,
failsafe,
gpuLayers,
autoGpuLayers,
handleBackendChange,
handleGpuDeviceChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
} = useLaunchConfig();
const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }>
>([]);
@ -44,10 +38,6 @@ export const BackendSelector = ({
const hasInitialized = useRef(false);
useEffect(() => {
if (hasInitialized.current) return;
let timeoutId: number;
const loadBackends = async () => {
try {
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
@ -83,16 +73,8 @@ export const BackendSelector = ({
setAvailableBackends(backends);
hasInitialized.current = true;
if (backends.length > 0 && !backend) {
timeoutId = window.setTimeout(() => {
onBackendChange(backends[0].value);
}, 10);
}
if (onBackendsReady) {
window.setTimeout(() => {
onBackendsReady();
}, 100);
}
} catch (error) {
window.electronAPI.logs.logError(
@ -107,14 +89,10 @@ export const BackendSelector = ({
}
};
if (!hasInitialized.current) {
loadBackends();
return () => {
if (timeoutId) {
window.clearTimeout(timeoutId);
}
};
}, [backend, onBackendChange, onBackendsReady]);
}, [onBackendsReady]);
useEffect(() => {
if (!onWarningsChange) return;
@ -130,7 +108,7 @@ export const BackendSelector = ({
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option to avoid crashes.',
'Your CPU does not support AVX2. Enable the "Disable AVX2" option on the Advanced tab to avoid crashes.',
});
}
@ -138,7 +116,7 @@ export const BackendSelector = ({
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option to avoid crashes.',
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option on the Advanced tab to avoid crashes.',
});
}
@ -178,7 +156,7 @@ export const BackendSelector = ({
value={backend}
onChange={(value) => {
if (value) {
onBackendChange(value);
handleBackendChange(value);
}
}}
data={availableBackends.map((b) => ({
@ -186,11 +164,6 @@ export const BackendSelector = ({
label: b.label,
}))}
disabled={availableBackends.length === 0}
comboboxProps={{
middlewares: {
flip: false,
},
}}
renderOption={({ option }) => {
const backendData = availableBackends.find(
(b) => b.value === option.value
@ -213,14 +186,13 @@ export const BackendSelector = ({
return (
isGpuBackend &&
onGpuDeviceChange &&
hasMultipleDevices && (
<Select
label="GPU Device"
placeholder="Select GPU device"
value={gpuDevice.toString()}
onChange={(value) =>
value && onGpuDeviceChange(parseInt(value, 10))
value && handleGpuDeviceChange(parseInt(value, 10))
}
data={selectedBackend.devices!.map((device, index) => ({
value: index.toString(),
@ -233,7 +205,6 @@ export const BackendSelector = ({
})()}
</div>
{onGpuLayersChange && onAutoGpuLayersChange && (
<div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
@ -245,7 +216,7 @@ export const BackendSelector = ({
<TextInput
value={gpuLayers.toString()}
onChange={(event) =>
onGpuLayersChange(Number(event.target.value) || 0)
handleGpuLayersChange(Number(event.target.value) || 0)
}
type="number"
min={0}
@ -260,7 +231,7 @@ export const BackendSelector = ({
label="Auto"
checked={autoGpuLayers}
onChange={(event) =>
onAutoGpuLayersChange(event.currentTarget.checked)
handleAutoGpuLayersChange(event.currentTarget.checked)
}
size="sm"
/>
@ -268,7 +239,6 @@ export const BackendSelector = ({
</Group>
</Group>
</div>
)}
</Group>
</div>
);

View file

@ -3,24 +3,10 @@ import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
import { getInputValidationState } from '@/utils';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css';
interface GeneralTabProps {
modelPath: string;
gpuLayers: number;
autoGpuLayers: boolean;
contextSize: number;
backend: string;
gpuDevice?: number;
noavx2: boolean;
failsafe: boolean;
onModelPathChange: (path: string) => void;
onSelectModelFile: () => void;
onGpuLayersChange: (layers: number) => void;
onAutoGpuLayersChange: (auto: boolean) => void;
onContextSizeChange: (size: number) => void;
onBackendChange: (backend: string) => void;
onGpuDeviceChange?: (device: number) => void;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
@ -28,24 +14,17 @@ interface GeneralTabProps {
}
export const GeneralTab = ({
modelPath,
gpuLayers,
autoGpuLayers,
contextSize,
backend,
gpuDevice,
noavx2,
failsafe,
onModelPathChange,
onSelectModelFile,
onGpuLayersChange,
onAutoGpuLayersChange,
onContextSizeChange,
onBackendChange,
onGpuDeviceChange,
onWarningsChange,
onBackendsReady,
}: GeneralTabProps) => {
const {
modelPath,
contextSize,
handleModelPathChange,
handleSelectModelFile,
handleContextSizeChangeWithStep,
} = useLaunchConfig();
const validationState = getInputValidationState(modelPath);
const getInputColor = () => {
@ -72,18 +51,8 @@ export const GeneralTab = ({
return (
<Stack gap="md">
<BackendSelector
backend={backend}
onBackendChange={onBackendChange}
gpuDevice={gpuDevice}
onGpuDeviceChange={onGpuDeviceChange}
noavx2={noavx2}
failsafe={failsafe}
onWarningsChange={onWarningsChange}
onBackendsReady={onBackendsReady}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
onGpuLayersChange={onGpuLayersChange}
onAutoGpuLayersChange={onAutoGpuLayersChange}
/>
<div>
@ -95,7 +64,7 @@ export const GeneralTab = ({
<TextInput
placeholder="Select a .gguf model file or enter a direct URL to file"
value={modelPath}
onChange={(event) => onModelPathChange(event.currentTarget.value)}
onChange={(e) => handleModelPathChange(e.target.value)}
color={getInputColor()}
error={
validationState === 'invalid' ? getHelperText() : undefined
@ -103,7 +72,7 @@ export const GeneralTab = ({
/>
</div>
<Button
onClick={onSelectModelFile}
onClick={handleSelectModelFile}
variant="light"
leftSection={<File size={16} />}
>
@ -134,7 +103,7 @@ export const GeneralTab = ({
<TextInput
value={contextSize?.toString() || ''}
onChange={(event) =>
onContextSizeChange(Number(event.target.value) || 256)
handleContextSizeChangeWithStep(Number(event.target.value) || 256)
}
type="number"
min={256}
@ -149,7 +118,7 @@ export const GeneralTab = ({
min={256}
max={131072}
step={1}
onChange={onContextSizeChange}
onChange={handleContextSizeChangeWithStep}
/>
</div>
</Stack>

View file

@ -3,34 +3,38 @@ import { useState } from 'react';
import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css';
interface ImageGenerationTabProps {
sdmodel: string;
sdt5xxl: string;
sdclipl: string;
sdclipg: string;
sdphotomaker: string;
sdvae: string;
sdlora: 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;
onSdloraChange: (path: string) => void;
onSelectSdloraFile: () => void;
onApplyPreset: (presetName: string) => void;
}
export const ImageGenerationTab = () => {
const {
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
sdlora,
handleSdmodelChange,
handleSelectSdmodelFile,
handleSdt5xxlChange,
handleSelectSdt5xxlFile,
handleSdcliplChange,
handleSelectSdcliplFile,
handleSdclipgChange,
handleSelectSdclipgFile,
handleSdphotomakerChange,
handleSelectSdphotomakerFile,
handleSdvaeChange,
handleSelectSdvaeFile,
handleSdloraChange,
handleSelectSdloraFile,
handleApplyPreset,
} = useLaunchConfig();
const ModelField = ({
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
const ModelField = ({
label,
value,
placeholder,
@ -38,7 +42,7 @@ const ModelField = ({
onChange,
onSelectFile,
showSearchHF = false,
}: {
}: {
label: string;
value: string;
placeholder: string;
@ -46,7 +50,7 @@ const ModelField = ({
onChange: (value: string) => void;
onSelectFile: () => void;
showSearchHF?: boolean;
}) => {
}) => {
const validationState = getInputValidationState(value);
const getInputColor = () => {
@ -85,7 +89,9 @@ const ModelField = ({
value={value}
onChange={(event) => onChange(event.currentTarget.value)}
color={getInputColor()}
error={validationState === 'invalid' ? getHelperText() : undefined}
error={
validationState === 'invalid' ? getHelperText() : undefined
}
/>
</div>
<Button
@ -111,33 +117,7 @@ const ModelField = ({
</Group>
</div>
);
};
export const ImageGenerationTab = ({
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
sdlora,
onSdmodelChange,
onSelectSdmodelFile,
onSdt5xxlChange,
onSelectSdt5xxlFile,
onSdcliplChange,
onSelectSdcliplFile,
onSdclipgChange,
onSelectSdclipgFile,
onSdphotomakerChange,
onSelectSdphotomakerFile,
onSdvaeChange,
onSelectSdvaeFile,
onSdloraChange,
onSelectSdloraFile,
onApplyPreset,
}: ImageGenerationTabProps) => {
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
};
return (
<Stack gap="md">
@ -158,7 +138,7 @@ export const ImageGenerationTab = ({
onChange={(value) => {
setSelectedPreset(value);
if (value) {
onApplyPreset(value);
handleApplyPreset(value);
}
}}
clearable
@ -169,8 +149,8 @@ export const ImageGenerationTab = ({
label="Image Gen. Model File"
value={sdmodel}
placeholder="Select a model file or enter a direct URL"
onChange={onSdmodelChange}
onSelectFile={onSelectSdmodelFile}
onChange={handleSdmodelChange}
onSelectFile={handleSelectSdmodelFile}
showSearchHF
/>
@ -178,24 +158,24 @@ export const ImageGenerationTab = ({
label="T5-XXL File"
value={sdt5xxl}
placeholder="Select a T5-XXL file or enter a direct URL"
onChange={onSdt5xxlChange}
onSelectFile={onSelectSdt5xxlFile}
onChange={handleSdt5xxlChange}
onSelectFile={handleSelectSdt5xxlFile}
/>
<ModelField
label="Clip-L File"
value={sdclipl}
placeholder="Select a Clip-L file or enter a direct URL"
onChange={onSdcliplChange}
onSelectFile={onSelectSdcliplFile}
onChange={handleSdcliplChange}
onSelectFile={handleSelectSdcliplFile}
/>
<ModelField
label="Clip-G File"
value={sdclipg}
placeholder="Select a Clip-G file or enter a direct URL"
onChange={onSdclipgChange}
onSelectFile={onSelectSdclipgFile}
onChange={handleSdclipgChange}
onSelectFile={handleSelectSdclipgFile}
/>
<ModelField
@ -203,16 +183,16 @@ export const ImageGenerationTab = ({
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}
onChange={handleSdphotomakerChange}
onSelectFile={handleSelectSdphotomakerFile}
/>
<ModelField
label="Image VAE"
value={sdvae}
placeholder="Select a VAE file or enter a direct URL"
onChange={onSdvaeChange}
onSelectFile={onSelectSdvaeFile}
onChange={handleSdvaeChange}
onSelectFile={handleSelectSdvaeFile}
/>
<ModelField
@ -220,8 +200,8 @@ export const ImageGenerationTab = ({
value={sdlora}
placeholder="Select a LoRa file or enter a direct URL"
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
onChange={onSdloraChange}
onSelectFile={onSelectSdloraFile}
onChange={handleSdloraChange}
onSelectFile={handleSelectSdloraFile}
/>
</Stack>
);

View file

@ -1,26 +1,11 @@
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css';
interface NetworkTabProps {
port: number | undefined;
host: string;
multiuser: boolean;
multiplayer: boolean;
remotetunnel: boolean;
nocertify: boolean;
websearch: boolean;
onPortChange: (port: number | undefined) => void;
onHostChange: (host: string) => void;
onMultiuserChange: (multiuser: boolean) => void;
onMultiplayerChange: (multiplayer: boolean) => void;
onRemotetunnelChange: (remotetunnel: boolean) => void;
onNocertifyChange: (nocertify: boolean) => void;
onWebsearchChange: (websearch: boolean) => void;
}
export const NetworkTab = ({
export const NetworkTab = () => {
const {
port,
host,
multiuser,
@ -28,14 +13,14 @@ export const NetworkTab = ({
remotetunnel,
nocertify,
websearch,
onPortChange,
onHostChange,
onMultiuserChange,
onMultiplayerChange,
onRemotetunnelChange,
onNocertifyChange,
onWebsearchChange,
}: NetworkTabProps) => {
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
} = useLaunchConfig();
const [portInput, setPortInput] = useState(port?.toString() ?? '');
useEffect(() => {
@ -53,10 +38,10 @@ export const NetworkTab = ({
<InfoTooltip label="The hostname or IP address on which KoboldCpp will bind its webserver to." />
</Group>
<TextInput
placeholder="localhost"
label="Host"
description="The IP address to bind to"
value={host}
onChange={(event) => onHostChange(event.currentTarget.value)}
style={{ maxWidth: 200 }}
onChange={(event) => handleHostChange(event.currentTarget.value)}
/>
</div>
@ -80,13 +65,13 @@ export const NetworkTab = ({
const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
onPortChange(numValue);
handlePortChange(numValue);
}
}}
onBlur={(event) => {
const value = event.currentTarget.value;
if (value === '') {
onPortChange(undefined);
handlePortChange(undefined);
setPortInput('');
}
}}
@ -106,7 +91,7 @@ export const NetworkTab = ({
<Checkbox
checked={multiuser}
onChange={(event) =>
onMultiuserChange(event.currentTarget.checked)
handleMultiuserChange(event.currentTarget.checked)
}
label="Multiuser Mode"
/>
@ -119,7 +104,7 @@ export const NetworkTab = ({
<Checkbox
checked={multiplayer}
onChange={(event) =>
onMultiplayerChange(event.currentTarget.checked)
handleMultiplayerChange(event.currentTarget.checked)
}
label="Shared Multiplayer"
/>
@ -134,7 +119,7 @@ export const NetworkTab = ({
<Checkbox
checked={remotetunnel}
onChange={(event) =>
onRemotetunnelChange(event.currentTarget.checked)
handleRemotetunnelChange(event.currentTarget.checked)
}
label="Remote Tunnel"
/>
@ -147,7 +132,7 @@ export const NetworkTab = ({
<Checkbox
checked={nocertify}
onChange={(event) =>
onNocertifyChange(event.currentTarget.checked)
handleNocertifyChange(event.currentTarget.checked)
}
label="No Certify Mode (Insecure)"
/>
@ -160,7 +145,7 @@ export const NetworkTab = ({
<Checkbox
checked={websearch}
onChange={(event) =>
onWebsearchChange(event.currentTarget.checked)
handleWebsearchChange(event.currentTarget.checked)
}
label="Enable WebSearch"
/>

View file

@ -1,10 +1,9 @@
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
import { useState, useEffect, useCallback } from 'react';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useTrackedConfigHandlers } from '@/hooks/useTrackedConfigHandlers';
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
@ -25,7 +24,6 @@ export const LaunchScreen = ({
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [, setInstallDir] = useState<string>('');
const [activeTab, setActiveTab] = useState<string | null>('general');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [warnings, setWarnings] = useState<
Array<{ type: 'warning' | 'info'; message: string }>
>([]);
@ -51,6 +49,7 @@ export const LaunchScreen = ({
quantmatmul,
backend,
gpuDevice,
gpuPlatform,
sdmodel,
sdt5xxl,
sdclipl,
@ -59,78 +58,9 @@ export const LaunchScreen = ({
sdvae,
sdlora,
parseAndApplyConfigFile,
loadSavedSettings,
loadConfigFromFile,
handleSelectModelFile,
handleGpuDeviceChange,
handleSelectSdmodelFile,
handleSelectSdt5xxlFile,
handleSelectSdcliplFile,
handleSelectSdclipgFile,
handleSelectSdphotomakerFile,
handleSelectSdvaeFile,
handleSelectSdloraFile,
handleApplyPreset,
handleModelPathChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
handleSdclipgChange,
handleSdphotomakerChange,
handleSdvaeChange,
handleSdloraChange,
} = useLaunchConfig();
const trackedHandlers = useTrackedConfigHandlers({
setHasUnsavedChanges,
handlers: {
handleModelPathChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
handleSdclipgChange,
handleSdphotomakerChange,
handleSdvaeChange,
handleSdloraChange,
},
});
const { isLaunching, handleLaunch } = useLaunchLogic({
modelPath,
sdmodel,
@ -156,13 +86,11 @@ export const LaunchScreen = ({
setSelectedFile(files[0].name);
}
await loadSavedSettings();
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
if (currentSelectedFile && !selectedFile) {
setSelectedFile(currentSelectedFile);
const loadedConfigFileName = await loadConfigFromFile(files, savedConfig);
if (loadedConfigFileName && !selectedFile) {
setSelectedFile(loadedConfigFileName);
}
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
}, [selectedFile, loadConfigFromFile]);
const handleFileSelection = async (fileName: string) => {
setSelectedFile(fileName);
@ -172,14 +100,19 @@ export const LaunchScreen = ({
if (selectedConfig) {
await parseAndApplyConfigFile(selectedConfig.path);
}
setHasUnsavedChanges(false);
};
const buildConfigData = () => ({
const buildConfigData = () => {
let useclblastValue: [number, number] | false = false;
if (backend === 'clblast') {
useclblastValue = [gpuDevice, gpuPlatform];
}
return {
gpulayers: gpuLayers,
contextsize: contextSize,
model_param: modelPath,
model: modelPath,
port,
host,
multiuser: multiuser ? 1 : 0,
@ -193,14 +126,15 @@ export const LaunchScreen = ({
failsafe,
usecuda: backend === 'cuda' || backend === 'rocm',
usevulkan: backend === 'vulkan',
useclblast: backend === 'clblast',
useclblast: useclblastValue,
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
});
};
};
const handleCreateNewConfig = async (configName: string) => {
try {
@ -214,7 +148,6 @@ export const LaunchScreen = ({
const newFileName = `${configName}.kcpps`;
setSelectedFile(newFileName);
await window.electronAPI.kobold.setSelectedConfig(newFileName);
setHasUnsavedChanges(false);
} else {
window.electronAPI.logs.logError(
'Failed to create new configuration',
@ -246,9 +179,7 @@ export const LaunchScreen = ({
buildConfigData()
);
if (success) {
setHasUnsavedChanges(false);
} else {
if (!success) {
window.electronAPI.logs.logError(
'Failed to save configuration',
new Error('Save operation failed')
@ -318,7 +249,6 @@ export const LaunchScreen = ({
<ConfigFileManager
configFiles={configFiles}
selectedFile={selectedFile}
hasUnsavedChanges={hasUnsavedChanges}
onFileSelection={handleFileSelection}
onCreateNewConfig={handleCreateNewConfig}
onSaveConfig={handleSaveConfig}
@ -335,138 +265,19 @@ export const LaunchScreen = ({
<div>
<Tabs.Panel value="general" pt="md">
<GeneralTab
modelPath={modelPath}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
contextSize={contextSize}
backend={backend}
gpuDevice={gpuDevice}
noavx2={noavx2}
failsafe={failsafe}
onModelPathChange={
trackedHandlers.handleModelPathChangeWithTracking
}
onSelectModelFile={handleSelectModelFile}
onGpuLayersChange={
trackedHandlers.handleGpuLayersChangeWithTracking
}
onAutoGpuLayersChange={
trackedHandlers.handleAutoGpuLayersChangeWithTracking
}
onContextSizeChange={
trackedHandlers.handleContextSizeChangeWithTracking
}
onBackendChange={
trackedHandlers.handleBackendChangeWithTracking
}
onGpuDeviceChange={handleGpuDeviceChange}
onWarningsChange={setWarnings}
/>
<GeneralTab onWarningsChange={setWarnings} />
</Tabs.Panel>
<Tabs.Panel value="advanced" pt="md">
<AdvancedTab
additionalArguments={additionalArguments}
noshift={noshift}
flashattention={flashattention}
noavx2={noavx2}
failsafe={failsafe}
lowvram={lowvram}
quantmatmul={quantmatmul}
backend={backend}
onAdditionalArgumentsChange={
trackedHandlers.handleAdditionalArgumentsChangeWithTracking
}
onNoshiftChange={
trackedHandlers.handleNoshiftChangeWithTracking
}
onFlashattentionChange={
trackedHandlers.handleFlashattentionChangeWithTracking
}
onNoavx2Change={
trackedHandlers.handleNoavx2ChangeWithTracking
}
onFailsafeChange={
trackedHandlers.handleFailsafeChangeWithTracking
}
onLowvramChange={
trackedHandlers.handleLowvramChangeWithTracking
}
onQuantmatmulChange={
trackedHandlers.handleQuantmatmulChangeWithTracking
}
/>
<AdvancedTab />
</Tabs.Panel>
<Tabs.Panel value="network" pt="md">
<NetworkTab
port={port}
host={host}
multiuser={multiuser}
multiplayer={multiplayer}
remotetunnel={remotetunnel}
nocertify={nocertify}
websearch={websearch}
onPortChange={trackedHandlers.handlePortChangeWithTracking}
onHostChange={trackedHandlers.handleHostChangeWithTracking}
onMultiuserChange={
trackedHandlers.handleMultiuserChangeWithTracking
}
onMultiplayerChange={
trackedHandlers.handleMultiplayerChangeWithTracking
}
onRemotetunnelChange={
trackedHandlers.handleRemotetunnelChangeWithTracking
}
onNocertifyChange={
trackedHandlers.handleNocertifyChangeWithTracking
}
onWebsearchChange={
trackedHandlers.handleWebsearchChangeWithTracking
}
/>
<NetworkTab />
</Tabs.Panel>
<Tabs.Panel value="image" pt="md">
<ImageGenerationTab
sdmodel={sdmodel}
sdt5xxl={sdt5xxl}
sdclipl={sdclipl}
sdclipg={sdclipg}
sdphotomaker={sdphotomaker}
sdvae={sdvae}
sdlora={sdlora}
onSdmodelChange={
trackedHandlers.handleSdmodelChangeWithTracking
}
onSelectSdmodelFile={handleSelectSdmodelFile}
onSdt5xxlChange={
trackedHandlers.handleSdt5xxlChangeWithTracking
}
onSelectSdt5xxlFile={handleSelectSdt5xxlFile}
onSdcliplChange={
trackedHandlers.handleSdcliplChangeWithTracking
}
onSelectSdcliplFile={handleSelectSdcliplFile}
onSdclipgChange={
trackedHandlers.handleSdclipgChangeWithTracking
}
onSelectSdclipgFile={handleSelectSdclipgFile}
onSdphotomakerChange={
trackedHandlers.handleSdphotomakerChangeWithTracking
}
onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
onSdvaeChange={
trackedHandlers.handleSdvaeChangeWithTracking
}
onSelectSdvaeFile={handleSelectSdvaeFile}
onSdloraChange={
trackedHandlers.handleSdloraChangeWithTracking
}
onSelectSdloraFile={handleSelectSdloraFile}
onApplyPreset={handleApplyPreset}
/>
<ImageGenerationTab />
</Tabs.Panel>
</div>
</Tabs>

View file

View file

@ -1,534 +1,90 @@
import { useState, useCallback } from 'react';
import type { ConfigFile } from '@/types';
import { useLaunchConfigStore } from '@/stores/launchConfigStore';
import {
getPresetByName,
IMAGE_MODEL_PRESETS,
type ImageModelPreset,
} from '@/utils/imageModelPresets';
import {
DEFAULT_CONTEXT_SIZE,
DEFAULT_MODEL_URL,
DEFAULT_HOST,
} from '@/constants';
export const useLaunchConfig = () => {
const [gpuLayers, setGpuLayers] = useState<number>(0);
const [autoGpuLayers, setAutoGpuLayers] = useState<boolean>(false);
const [contextSize, setContextSize] = useState<number>(DEFAULT_CONTEXT_SIZE);
const [modelPath, setModelPath] = useState<string>(DEFAULT_MODEL_URL);
const [additionalArguments, setAdditionalArguments] = useState<string>('');
const [port, setPort] = useState<number | undefined>(undefined);
const [host, setHost] = useState<string>(DEFAULT_HOST);
const [multiuser, setMultiuser] = useState<boolean>(false);
const [multiplayer, setMultiplayer] = useState<boolean>(false);
const [remotetunnel, setRemotetunnel] = useState<boolean>(false);
const [nocertify, setNocertify] = useState<boolean>(false);
const [websearch, setWebsearch] = useState<boolean>(false);
const [noshift, setNoshift] = useState<boolean>(false);
const [flashattention, setFlashattention] = useState<boolean>(true);
const [noavx2, setNoavx2] = useState<boolean>(false);
const [failsafe, setFailsafe] = useState<boolean>(false);
const [lowvram, setLowvram] = useState<boolean>(false);
const [quantmatmul, setQuantmatmul] = useState<boolean>(true);
const [backend, setBackend] = useState<string>('');
const [gpuDevice, setGpuDevice] = useState<number>(0);
const [sdmodel, setSdmodel] = useState<string>('');
const [sdt5xxl, setSdt5xxl] = useState<string>('');
const [sdclipl, setSdclipl] = useState<string>('');
const [sdclipg, setSdclipg] = useState<string>('');
const [sdphotomaker, setSdphotomaker] = useState<string>('');
const [sdvae, setSdvae] = useState<string>('');
const [sdlora, setSdlora] = useState<string>('');
// eslint-disable-next-line sonarjs/cognitive-complexity
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
const configData =
await window.electronAPI.kobold.parseConfigFile(configPath);
if (configData) {
if (typeof configData.gpulayers === 'number') {
setGpuLayers(configData.gpulayers);
} else {
setGpuLayers(0);
}
if (typeof configData.contextsize === 'number') {
setContextSize(configData.contextsize);
} else {
setContextSize(DEFAULT_CONTEXT_SIZE);
}
if (typeof configData.model_param === 'string') {
setModelPath(configData.model_param);
}
if (typeof configData.port === 'number') {
setPort(configData.port);
} else {
setPort(undefined);
}
if (typeof configData.host === 'string') {
setHost(configData.host);
} else {
setHost(DEFAULT_HOST);
}
if (typeof configData.multiuser === 'number') {
setMultiuser(configData.multiuser === 1);
} else {
setMultiuser(false);
}
if (typeof configData.multiplayer === 'boolean') {
setMultiplayer(configData.multiplayer);
} else {
setMultiplayer(false);
}
if (typeof configData.remotetunnel === 'boolean') {
setRemotetunnel(configData.remotetunnel);
} else {
setRemotetunnel(false);
}
if (typeof configData.nocertify === 'boolean') {
setNocertify(configData.nocertify);
} else {
setNocertify(false);
}
if (typeof configData.websearch === 'boolean') {
setWebsearch(configData.websearch);
} else {
setWebsearch(false);
}
if (typeof configData.noshift === 'boolean') {
setNoshift(configData.noshift);
} else {
setNoshift(false);
}
if (typeof configData.flashattention === 'boolean') {
setFlashattention(configData.flashattention);
} else {
setFlashattention(true);
}
if (typeof configData.noavx2 === 'boolean') {
setNoavx2(configData.noavx2);
} else {
setNoavx2(false);
}
if (typeof configData.failsafe === 'boolean') {
setFailsafe(configData.failsafe);
} else {
setFailsafe(false);
}
if (typeof configData.lowvram === 'boolean') {
setLowvram(configData.lowvram);
}
if (typeof configData.quantmatmul === 'boolean') {
setQuantmatmul(configData.quantmatmul);
}
if (configData.usecuda === true) {
const gpuInfo = await window.electronAPI.kobold.detectGPU();
setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm');
if (
Array.isArray(configData.usecuda) &&
configData.usecuda.length >= 3
) {
const [vramMode, deviceId, mmqMode] = configData.usecuda;
setLowvram(vramMode === 'lowvram');
setGpuDevice(parseInt(deviceId, 10) || 0);
setQuantmatmul(mmqMode === 'mmq');
}
} else if (configData.usevulkan === true) {
setBackend('vulkan');
} else if (configData.useclblast === true) {
setBackend('clblast');
} else {
setBackend('cpu');
}
if (typeof configData.sdmodel === 'string') {
setSdmodel(configData.sdmodel);
}
if (typeof configData.sdt5xxl === 'string') {
setSdt5xxl(configData.sdt5xxl);
}
if (typeof configData.sdclipl === 'string') {
setSdclipl(configData.sdclipl);
}
if (typeof configData.sdclipg === 'string') {
setSdclipg(configData.sdclipg);
}
if (typeof configData.sdphotomaker === 'string') {
setSdphotomaker(configData.sdphotomaker);
}
if (typeof configData.sdvae === 'string') {
setSdvae(configData.sdvae);
}
if (typeof configData.sdlora === 'string') {
setSdlora(configData.sdlora);
}
} else {
const cpuCapabilities = await window.electronAPI.kobold.detectCPU();
setGpuLayers(0);
setContextSize(DEFAULT_CONTEXT_SIZE);
setPort(undefined);
setHost(DEFAULT_HOST);
setMultiuser(false);
setMultiplayer(false);
setRemotetunnel(false);
setNocertify(false);
setWebsearch(false);
setNoshift(false);
setFlashattention(true);
setNoavx2(!cpuCapabilities.avx2);
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
setBackend('');
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 cpuCapabilities = await window.electronAPI.kobold.detectCPU();
setModelPath(DEFAULT_MODEL_URL);
setGpuLayers(0);
setContextSize(DEFAULT_CONTEXT_SIZE);
setPort(undefined);
setHost(DEFAULT_HOST);
setMultiuser(false);
setMultiplayer(false);
setRemotetunnel(false);
setNocertify(false);
setWebsearch(false);
setNoshift(false);
setFlashattention(true);
setNoavx2(!cpuCapabilities.avx2);
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
setBackend('');
setSdmodel('');
setSdt5xxl('');
setSdclipl('');
setSdclipg('');
setSdphotomaker('');
setSdvae('');
}, []);
const loadConfigFromFile = useCallback(
async (configFiles: ConfigFile[], savedConfig: string | null) => {
let currentSelectedFile = null;
if (savedConfig && configFiles.some((f) => f.name === savedConfig)) {
currentSelectedFile = savedConfig;
} else if (configFiles.length > 0) {
currentSelectedFile = configFiles[0].name;
}
if (currentSelectedFile) {
const selectedConfig = configFiles.find(
(f) => f.name === currentSelectedFile
);
if (selectedConfig) {
await parseAndApplyConfigFile(selectedConfig.path);
}
}
return currentSelectedFile;
},
[parseAndApplyConfigFile]
);
const handleGpuLayersChange = useCallback(async (value: number) => {
setGpuLayers(value);
}, []);
const roundToValidContextSize = useCallback((value: number): number => {
if (value < 1024) {
return Math.round(value / 256) * 256;
}
return Math.round(value / 1024) * 1024;
}, []);
const handleContextSizeChangeWithStep = useCallback(
async (value: number) => {
const roundedValue = roundToValidContextSize(value);
setContextSize(roundedValue);
},
[roundToValidContextSize]
);
const handleModelPathChange = useCallback((value: string) => {
setModelPath(value);
}, []);
const handleSelectModelFile = useCallback(async () => {
const filePath = await window.electronAPI.kobold.selectModelFile();
if (filePath) {
handleModelPathChange(filePath);
}
}, [handleModelPathChange]);
const handleAdditionalArgumentsChange = useCallback((value: string) => {
setAdditionalArguments(value);
}, []);
const handleAutoGpuLayersChange = useCallback((checked: boolean) => {
setAutoGpuLayers(checked);
}, []);
const handlePortChange = useCallback((value: number | undefined) => {
setPort(value);
}, []);
const handleHostChange = useCallback((value: string) => {
setHost(value);
}, []);
const handleMultiuserChange = useCallback((checked: boolean) => {
setMultiuser(checked);
}, []);
const handleMultiplayerChange = useCallback((checked: boolean) => {
setMultiplayer(checked);
}, []);
const handleRemotetunnelChange = useCallback((checked: boolean) => {
setRemotetunnel(checked);
}, []);
const handleNocertifyChange = useCallback((checked: boolean) => {
setNocertify(checked);
}, []);
const handleWebsearchChange = useCallback((checked: boolean) => {
setWebsearch(checked);
}, []);
const handleNoshiftChange = useCallback((checked: boolean) => {
setNoshift(checked);
}, []);
const handleFlashattentionChange = useCallback((checked: boolean) => {
setFlashattention(checked);
}, []);
const handleNoavx2Change = useCallback((checked: boolean) => {
setNoavx2(checked);
}, []);
const handleFailsafeChange = useCallback((checked: boolean) => {
setFailsafe(checked);
}, []);
const handleLowvramChange = useCallback((checked: boolean) => {
setLowvram(checked);
}, []);
const handleQuantmatmulChange = useCallback((checked: boolean) => {
setQuantmatmul(checked);
}, []);
const handleBackendChange = useCallback((backend: string) => {
setBackend(backend);
}, []);
const handleGpuDeviceChange = useCallback((device: number) => {
setGpuDevice(device);
}, []);
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 handleSdloraChange = useCallback((path: string) => {
setSdlora(path);
}, []);
const handleSelectSdloraFile = useCallback(async () => {
const filePath = await window.electronAPI.kobold.selectModelFile();
if (filePath) {
setSdlora(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]
);
const state = useLaunchConfigStore();
return {
gpuLayers,
autoGpuLayers,
contextSize,
modelPath,
additionalArguments,
port,
host,
multiuser,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
noavx2,
failsafe,
lowvram,
quantmatmul,
backend,
gpuDevice,
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
sdlora,
gpuLayers: state.gpuLayers,
autoGpuLayers: state.autoGpuLayers,
contextSize: state.contextSize,
modelPath: state.modelPath,
additionalArguments: state.additionalArguments,
port: state.port,
host: state.host,
multiuser: state.multiuser,
multiplayer: state.multiplayer,
remotetunnel: state.remotetunnel,
nocertify: state.nocertify,
websearch: state.websearch,
noshift: state.noshift,
flashattention: state.flashattention,
noavx2: state.noavx2,
failsafe: state.failsafe,
lowvram: state.lowvram,
quantmatmul: state.quantmatmul,
backend: state.backend,
gpuDevice: state.gpuDevice,
gpuPlatform: state.gpuPlatform,
sdmodel: state.sdmodel,
sdt5xxl: state.sdt5xxl,
sdclipl: state.sdclipl,
sdclipg: state.sdclipg,
sdphotomaker: state.sdphotomaker,
sdvae: state.sdvae,
sdlora: state.sdlora,
parseAndApplyConfigFile,
loadSavedSettings,
loadConfigFromFile,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleModelPathChange,
handleSelectModelFile,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleGpuDeviceChange,
handleSdmodelChange,
handleSelectSdmodelFile,
handleSdt5xxlChange,
handleSelectSdt5xxlFile,
handleSdcliplChange,
handleSelectSdcliplFile,
handleSdclipgChange,
handleSelectSdclipgFile,
handleSdphotomakerChange,
handleSelectSdphotomakerFile,
handleSdvaeChange,
handleSelectSdvaeFile,
handleSdloraChange,
handleSelectSdloraFile,
applyImageModelPreset,
handleApplyPreset,
handleGpuLayersChange: state.setGpuLayers,
handleAutoGpuLayersChange: state.setAutoGpuLayers,
handleContextSizeChangeWithStep: state.contextSizeChangeWithStep,
handleModelPathChange: state.setModelPath,
handleAdditionalArgumentsChange: state.setAdditionalArguments,
handlePortChange: state.setPort,
handleHostChange: state.setHost,
handleMultiuserChange: state.setMultiuser,
handleMultiplayerChange: state.setMultiplayer,
handleRemotetunnelChange: state.setRemotetunnel,
handleNocertifyChange: state.setNocertify,
handleWebsearchChange: state.setWebsearch,
handleNoshiftChange: state.setNoshift,
handleFlashattentionChange: state.setFlashattention,
handleNoavx2Change: state.setNoavx2,
handleFailsafeChange: state.setFailsafe,
handleLowvramChange: state.setLowvram,
handleQuantmatmulChange: state.setQuantmatmul,
handleBackendChange: state.setBackend,
handleGpuDeviceChange: state.setGpuDevice,
handleGpuPlatformChange: state.setGpuPlatform,
handleSdmodelChange: state.setSdmodel,
handleSdt5xxlChange: state.setSdt5xxl,
handleSdcliplChange: state.setSdclipl,
handleSdclipgChange: state.setSdclipg,
handleSdphotomakerChange: state.setSdphotomaker,
handleSdvaeChange: state.setSdvae,
handleSdloraChange: state.setSdlora,
parseAndApplyConfigFile: state.parseAndApplyConfigFile,
loadConfigFromFile: state.loadConfigFromFile,
handleSelectModelFile: state.selectModelFile,
handleImageModelPresetChange: state.applyImageModelPreset,
handleApplyPreset: (presetName: string) => {
const preset = IMAGE_MODEL_PRESETS.find(
(p: ImageModelPreset) => p.name === presetName
);
if (preset) {
state.applyImageModelPreset(preset);
}
},
handleSelectSdmodelFile: state.selectSdmodelFile,
handleSelectSdt5xxlFile: state.selectSdt5xxlFile,
handleSelectSdcliplFile: state.selectSdcliplFile,
handleSelectSdclipgFile: state.selectSdclipgFile,
handleSelectSdphotomakerFile: state.selectSdphotomakerFile,
handleSelectSdvaeFile: state.selectSdvaeFile,
handleSelectSdloraFile: state.selectSdloraFile,
};
};

View file

@ -1,4 +1,5 @@
import { useState, useCallback } from 'react';
import { parseCLBlastDevice } from '@/utils';
interface UseLaunchLogicProps {
modelPath: string;
@ -22,7 +23,7 @@ interface LaunchArgs {
flashattention: boolean;
backend: string;
lowvram: boolean;
gpuDevice: number;
gpuDevice: number | string;
quantmatmul: boolean;
additionalArguments: string;
sdt5xxl: string;
@ -116,13 +117,33 @@ const buildBackendArgs = (launchArgs: LaunchArgs): string[] => {
if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') {
const cudaArgs = ['--usecuda'];
cudaArgs.push(launchArgs.lowvram ? 'lowvram' : 'normal');
cudaArgs.push(launchArgs.gpuDevice.toString());
cudaArgs.push(
typeof launchArgs.gpuDevice === 'string'
? '0'
: launchArgs.gpuDevice.toString()
);
cudaArgs.push(launchArgs.quantmatmul ? 'mmq' : 'nommq');
args.push(...cudaArgs);
} else if (launchArgs.backend === 'vulkan') {
args.push('--usevulkan');
} else if (launchArgs.backend === 'clblast') {
args.push('--useclblast');
const clblastArgs = ['--useclblast'];
if (typeof launchArgs.gpuDevice === 'string') {
const parsed = parseCLBlastDevice(launchArgs.gpuDevice);
if (parsed) {
clblastArgs.push(
parsed.deviceIndex.toString(),
parsed.platformIndex.toString()
);
} else {
clblastArgs.push('0', '0');
}
} else {
clblastArgs.push(launchArgs.gpuDevice.toString(), '0');
}
args.push(...clblastArgs);
}
}

View file

@ -22,6 +22,8 @@ interface TrackedConfigHandlersProps {
handleLowvramChange: (enabled: boolean) => void;
handleQuantmatmulChange: (enabled: boolean) => void;
handleBackendChange: (backend: string) => void;
handleGpuDeviceChange: (device: number) => void;
handleGpuPlatformChange: (platform: number) => void;
handleSdmodelChange: (path: string) => void;
handleSdt5xxlChange: (path: string) => void;
handleSdcliplChange: (path: string) => void;
@ -58,6 +60,8 @@ export const useTrackedConfigHandlers = ({
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleGpuDeviceChange,
handleGpuPlatformChange,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
@ -144,6 +148,14 @@ export const useTrackedConfigHandlers = ({
handleBackendChange,
setHasUnsavedChanges
),
handleGpuDeviceChangeWithTracking: createChangeTracker(
handleGpuDeviceChange,
setHasUnsavedChanges
),
handleGpuPlatformChangeWithTracking: createChangeTracker(
handleGpuPlatformChange,
setHasUnsavedChanges
),
handleSdmodelChangeWithTracking: createChangeTracker(
handleSdmodelChange,
setHasUnsavedChanges

View file

@ -348,7 +348,7 @@ export class KoboldCppManager {
async parseConfigFile(filePath: string): Promise<{
gpulayers?: number;
contextsize?: number;
model_param?: string;
model?: string;
[key: string]: unknown;
} | null> {
try {
@ -371,7 +371,7 @@ export class KoboldCppManager {
configData: {
gpulayers?: number;
contextsize?: number;
model_param?: string;
model?: string;
port?: number;
host?: string;
multiuser?: number;
@ -385,7 +385,7 @@ export class KoboldCppManager {
failsafe?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
useclblast?: [number, number] | boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
@ -615,6 +615,7 @@ export class KoboldCppManager {
this.koboldProcess = spawn(versionPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
shell: process.platform === 'win32',
});
if (onOutput) {
@ -938,6 +939,7 @@ export class KoboldCppManager {
const child = spawn(currentVersion.path, finalArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
shell: process.platform === 'win32',
});
this.koboldProcess = child;

View file

@ -13,6 +13,10 @@ export interface BackendSupport {
export class BinaryService {
private backendSupportCache = new Map<string, BackendSupport>();
private availableBackendsCache = new Map<
string,
Array<{ value: string; label: string; devices?: string[] }>
>();
private logManager: LogManager;
constructor(logManager: LogManager) {
@ -78,6 +82,12 @@ export class BinaryService {
clblast: { supported: boolean; devices: string[] };
}
): Array<{ value: string; label: string; devices?: string[] }> {
const cacheKey = `${koboldBinaryPath}:${JSON.stringify(hardwareCapabilities)}`;
if (this.availableBackendsCache.has(cacheKey)) {
return this.availableBackendsCache.get(cacheKey)!;
}
const backendSupport = this.detectBackendSupport(koboldBinaryPath);
const backends: Array<{
value: string;
@ -122,10 +132,12 @@ export class BinaryService {
label: 'CPU',
});
this.availableBackendsCache.set(cacheKey, backends);
return backends;
}
clearCache(): void {
this.backendSupportCache.clear();
this.availableBackendsCache.clear();
}
}

View file

@ -1,5 +1,6 @@
/* eslint-disable no-comments/disallowComments */
import si from 'systeminformation';
import * as openclInfo from 'opencl-info';
import { shortenDeviceName } from '@/utils';
import type {
CPUCapabilities,
@ -343,74 +344,35 @@ export class HardwareService {
devices: string[];
}> {
try {
const { spawn } = await import('child_process');
const clinfo = spawn('clinfo', ['--json'], { timeout: 5000 });
const platforms = openclInfo.getPlatformInfo();
let output = '';
clinfo.stdout.on('data', (data) => {
output += data.toString();
});
return new Promise((resolve) => {
// eslint-disable-next-line sonarjs/cognitive-complexity
clinfo.on('close', (code) => {
if (code === 0 && output.trim()) {
try {
const data = JSON.parse(output);
const devices: string[] = [];
if (data.platforms) {
for (const platform of data.platforms) {
for (
let platformIndex = 0;
platformIndex < platforms.length;
platformIndex++
) {
const platform = platforms[platformIndex];
if (platform.devices) {
for (const device of platform.devices) {
if (device.name && device.type !== 'CPU') {
devices.push(shortenDeviceName(device.name));
}
for (
let deviceIndex = 0;
deviceIndex < platform.devices.length;
deviceIndex++
) {
const device = platform.devices[deviceIndex];
if (device.name && device.type === 'GPU') {
const deviceLabel = `${shortenDeviceName(device.name)} (${platform.name})`;
devices.push(deviceLabel);
}
}
}
}
resolve({
return {
supported: devices.length > 0,
devices,
});
} catch {
const lines = output.split('\n');
const devices: string[] = [];
for (const line of lines) {
if (line.includes('Device Name') && !line.includes('CPU')) {
const name = line.split(':')[1]?.trim();
if (name) {
devices.push(shortenDeviceName(name));
}
}
}
resolve({
supported: devices.length > 0,
devices,
});
}
} else {
resolve({ supported: false, devices: [] });
}
});
clinfo.on('error', () => {
resolve({ supported: false, devices: [] });
});
setTimeout(() => {
try {
clinfo.kill('SIGTERM');
} catch {
void 0;
}
resolve({ supported: false, devices: [] });
}, 5000);
});
};
} catch {
return { supported: false, devices: [] };
}

View file

@ -55,7 +55,7 @@ const koboldAPI: KoboldAPI = {
configData: {
gpulayers?: number;
contextsize?: number;
model_param?: string;
model?: string;
port?: number;
host?: string;
multiuser?: number;
@ -69,7 +69,7 @@ const koboldAPI: KoboldAPI = {
failsafe?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
useclblast?: [number, number] | boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;

View file

@ -0,0 +1,393 @@
import { create } from 'zustand';
import type { ConfigFile } from '@/types';
import type { ImageModelPreset } from '@/utils/imageModelPresets';
import {
DEFAULT_CONTEXT_SIZE,
DEFAULT_MODEL_URL,
DEFAULT_HOST,
} from '@/constants';
interface LaunchConfigState {
gpuLayers: number;
autoGpuLayers: boolean;
contextSize: number;
modelPath: string;
additionalArguments: string;
port?: number;
host: string;
multiuser: boolean;
multiplayer: boolean;
remotetunnel: boolean;
nocertify: boolean;
websearch: boolean;
noshift: boolean;
flashattention: boolean;
noavx2: boolean;
failsafe: boolean;
lowvram: boolean;
quantmatmul: boolean;
backend: string;
gpuDevice: number;
gpuPlatform: number;
sdmodel: string;
sdt5xxl: string;
sdclipl: string;
sdclipg: string;
sdphotomaker: string;
sdvae: string;
sdlora: string;
setGpuLayers: (layers: number) => void;
setAutoGpuLayers: (auto: boolean) => void;
setContextSize: (size: number) => void;
setModelPath: (path: string) => void;
setAdditionalArguments: (args: string) => void;
setPort: (port?: number) => void;
setHost: (host: string) => void;
setMultiuser: (multiuser: boolean) => void;
setMultiplayer: (multiplayer: boolean) => void;
setRemotetunnel: (remotetunnel: boolean) => void;
setNocertify: (nocertify: boolean) => void;
setWebsearch: (websearch: boolean) => void;
setNoshift: (noshift: boolean) => void;
setFlashattention: (flashattention: boolean) => void;
setNoavx2: (noavx2: boolean) => void;
setFailsafe: (failsafe: boolean) => void;
setLowvram: (lowvram: boolean) => void;
setQuantmatmul: (quantmatmul: boolean) => void;
setBackend: (backend: string) => void;
setGpuDevice: (device: number) => void;
setGpuPlatform: (platform: number) => void;
setSdmodel: (model: string) => void;
setSdt5xxl: (model: string) => void;
setSdclipl: (model: string) => void;
setSdclipg: (model: string) => void;
setSdphotomaker: (model: string) => void;
setSdvae: (vae: string) => void;
setSdlora: (loraModel: string) => void;
parseAndApplyConfigFile: (configPath: string) => Promise<void>;
loadConfigFromFile: (
configFiles: ConfigFile[],
savedConfig: string | null
) => Promise<string | null>;
selectModelFile: () => Promise<void>;
selectSdmodelFile: () => Promise<void>;
selectSdt5xxlFile: () => Promise<void>;
selectSdcliplFile: () => Promise<void>;
selectSdclipgFile: () => Promise<void>;
selectSdphotomakerFile: () => Promise<void>;
selectSdvaeFile: () => Promise<void>;
selectSdloraFile: () => Promise<void>;
contextSizeChangeWithStep: (size: number) => void;
applyImageModelPreset: (preset: ImageModelPreset) => void;
}
export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
gpuLayers: 0,
autoGpuLayers: false,
contextSize: DEFAULT_CONTEXT_SIZE,
modelPath: DEFAULT_MODEL_URL,
additionalArguments: '',
port: undefined,
host: DEFAULT_HOST,
multiuser: false,
multiplayer: false,
remotetunnel: false,
nocertify: false,
websearch: false,
noshift: false,
flashattention: true,
noavx2: false,
failsafe: false,
lowvram: false,
quantmatmul: true,
backend: '',
gpuDevice: 0,
gpuPlatform: 0,
sdmodel: '',
sdt5xxl: '',
sdclipl: '',
sdclipg: '',
sdphotomaker: '',
sdvae: '',
sdlora: '',
setGpuLayers: (layers) => set({ gpuLayers: layers }),
setAutoGpuLayers: (auto) => set({ autoGpuLayers: auto }),
setContextSize: (size) => set({ contextSize: size }),
setModelPath: (path) => set({ modelPath: path }),
setAdditionalArguments: (args) => set({ additionalArguments: args }),
setPort: (port) => set({ port }),
setHost: (host) => set({ host }),
setMultiuser: (multiuser) => set({ multiuser }),
setMultiplayer: (multiplayer) => set({ multiplayer }),
setRemotetunnel: (remotetunnel) => set({ remotetunnel }),
setNocertify: (nocertify) => set({ nocertify }),
setWebsearch: (websearch) => set({ websearch }),
setNoshift: (noshift) => set({ noshift }),
setFlashattention: (flashattention) => set({ flashattention }),
setNoavx2: (noavx2) => set({ noavx2 }),
setFailsafe: (failsafe) => set({ failsafe }),
setLowvram: (lowvram) => set({ lowvram }),
setQuantmatmul: (quantmatmul) => set({ quantmatmul }),
setBackend: (backend) => set({ backend }),
setGpuDevice: (device) => set({ gpuDevice: device }),
setGpuPlatform: (platform) => set({ gpuPlatform: platform }),
setSdmodel: (model) => set({ sdmodel: model }),
setSdt5xxl: (model) => set({ sdt5xxl: model }),
setSdclipl: (model) => set({ sdclipl: model }),
setSdclipg: (model) => set({ sdclipg: model }),
setSdphotomaker: (model) => set({ sdphotomaker: model }),
setSdvae: (vae) => set({ sdvae: vae }),
setSdlora: (loraModel) => set({ sdlora: loraModel }),
// eslint-disable-next-line sonarjs/cognitive-complexity
parseAndApplyConfigFile: async (configPath: string) => {
const configData =
await window.electronAPI.kobold.parseConfigFile(configPath);
if (configData) {
const updates: Partial<LaunchConfigState> = {};
if (typeof configData.gpulayers === 'number') {
updates.gpuLayers = configData.gpulayers;
} else {
updates.gpuLayers = 0;
}
if (typeof configData.contextsize === 'number') {
updates.contextSize = configData.contextsize;
} else {
updates.contextSize = DEFAULT_CONTEXT_SIZE;
}
if (typeof configData.model === 'string') {
updates.modelPath = configData.model;
}
if (typeof configData.port === 'number') {
updates.port = configData.port;
} else {
updates.port = undefined;
}
if (typeof configData.host === 'string') {
updates.host = configData.host;
} else {
updates.host = DEFAULT_HOST;
}
if (typeof configData.multiuser === 'number') {
updates.multiuser = configData.multiuser === 1;
} else {
updates.multiuser = false;
}
if (typeof configData.multiplayer === 'boolean') {
updates.multiplayer = configData.multiplayer;
} else {
updates.multiplayer = false;
}
if (typeof configData.remotetunnel === 'boolean') {
updates.remotetunnel = configData.remotetunnel;
} else {
updates.remotetunnel = false;
}
if (typeof configData.nocertify === 'boolean') {
updates.nocertify = configData.nocertify;
} else {
updates.nocertify = false;
}
if (typeof configData.websearch === 'boolean') {
updates.websearch = configData.websearch;
} else {
updates.websearch = false;
}
if (typeof configData.noshift === 'boolean') {
updates.noshift = configData.noshift;
} else {
updates.noshift = false;
}
if (typeof configData.flashattention === 'boolean') {
updates.flashattention = configData.flashattention;
} else {
updates.flashattention = true;
}
if (typeof configData.noavx2 === 'boolean') {
updates.noavx2 = configData.noavx2;
} else {
updates.noavx2 = false;
}
if (typeof configData.failsafe === 'boolean') {
updates.failsafe = configData.failsafe;
} else {
updates.failsafe = false;
}
if (typeof configData.lowvram === 'boolean') {
updates.lowvram = configData.lowvram;
}
if (typeof configData.quantmatmul === 'boolean') {
updates.quantmatmul = configData.quantmatmul;
}
if (configData.usecuda === true) {
const gpuInfo = await window.electronAPI.kobold.detectGPU();
updates.backend = gpuInfo.hasNVIDIA ? 'cuda' : 'rocm';
if (
Array.isArray(configData.usecuda) &&
configData.usecuda.length >= 3
) {
const [vramMode, deviceId, mmqMode] = configData.usecuda;
updates.lowvram = vramMode === 'lowvram';
updates.gpuDevice = parseInt(deviceId, 10) || 0;
updates.quantmatmul = mmqMode === 'mmq';
}
} else if (configData.usevulkan === true) {
updates.backend = 'vulkan';
} else if (
Array.isArray(configData.useclblast) &&
configData.useclblast.length === 2
) {
updates.backend = 'clblast';
const [deviceIndex, platformIndex] = configData.useclblast;
updates.gpuDevice = deviceIndex;
updates.gpuPlatform = platformIndex;
} else {
updates.backend = 'cpu';
}
if (typeof configData.sdmodel === 'string') {
updates.sdmodel = configData.sdmodel;
}
if (typeof configData.sdt5xxl === 'string') {
updates.sdt5xxl = configData.sdt5xxl;
}
if (typeof configData.sdclipl === 'string') {
updates.sdclipl = configData.sdclipl;
}
if (typeof configData.sdclipg === 'string') {
updates.sdclipg = configData.sdclipg;
}
if (typeof configData.sdphotomaker === 'string') {
updates.sdphotomaker = configData.sdphotomaker;
}
if (typeof configData.sdvae === 'string') {
updates.sdvae = configData.sdvae;
}
if (typeof configData.sdlora === 'string') {
updates.sdlora = configData.sdlora;
}
set(updates);
}
},
loadConfigFromFile: async (
configFiles: ConfigFile[],
savedConfig: string | null
): Promise<string | null> => {
let currentSelectedFile = null;
if (savedConfig) {
currentSelectedFile = configFiles.find((f) => f.name === savedConfig);
}
if (!currentSelectedFile && configFiles.length > 0) {
currentSelectedFile = configFiles[0];
}
if (currentSelectedFile) {
await get().parseAndApplyConfigFile(currentSelectedFile.path);
return currentSelectedFile.name;
}
return null;
},
selectModelFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ modelPath: result });
}
},
selectSdmodelFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdmodel: result });
}
},
selectSdt5xxlFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdt5xxl: result });
}
},
selectSdcliplFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdclipl: result });
}
},
selectSdclipgFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdclipg: result });
}
},
selectSdphotomakerFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdphotomaker: result });
}
},
selectSdvaeFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdvae: result });
}
},
selectSdloraFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile();
if (result) {
set({ sdlora: result });
}
},
contextSizeChangeWithStep: (size: number) => {
const roundedSize = Math.round(size / 256) * 256;
set({ contextSize: Math.max(256, Math.min(131072, roundedSize)) });
},
applyImageModelPreset: (preset: ImageModelPreset) => {
set({
sdt5xxl: preset.sdt5xxl,
sdclipl: preset.sdclipl,
sdclipg: preset.sdclipg || '',
sdvae: preset.sdvae,
});
},
}));

View file

@ -110,7 +110,7 @@ export interface KoboldAPI {
configData: {
gpulayers?: number;
contextsize?: number;
model_param?: string;
model?: string;
port?: number;
host?: string;
multiuser?: number;
@ -124,7 +124,7 @@ export interface KoboldAPI {
failsafe?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
useclblast?: [number, number] | boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
@ -139,7 +139,7 @@ export interface KoboldAPI {
parseConfigFile: (filePath: string) => Promise<{
gpulayers?: number;
contextsize?: number;
model_param?: string;
model?: string;
[key: string]: unknown;
} | null>;
selectModelFile: () => Promise<string | null>;

25
src/utils/clblast.ts Normal file
View file

@ -0,0 +1,25 @@
export function parseCLBlastDevice(deviceString: string): {
deviceIndex: number;
platformIndex: number;
} | null {
const match = deviceString.match(/\[(\d+),(\d+)\]$/);
if (match) {
return {
deviceIndex: parseInt(match[1], 10),
platformIndex: parseInt(match[2], 10),
};
}
return null;
}
export function formatCLBlastArgs(
deviceIndex: number,
platformIndex: number
): [string, string] {
return [deviceIndex.toString(), platformIndex.toString()];
}
export function getCLBlastDeviceName(deviceString: string): string {
const match = deviceString.match(/^(.+?)\s+\(Platform:/);
return match ? match[1].trim() : deviceString;
}

View file

@ -1,4 +1,5 @@
export * from './assets';
export * from './clblast';
export * from './downloadUtils';
export * from './fileSize';
export * from './hardware';

912
yarn.lock

File diff suppressed because it is too large Load diff