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", "KOBOLDAI",
"koboldcpp", "koboldcpp",
"KoboldCpp", "KoboldCpp",
"lora",
"lowvram", "lowvram",
"Lowvram", "Lowvram",
"maximizable", "maximizable",
@ -50,6 +51,7 @@
"nvidia", "nvidia",
"oldpc", "oldpc",
"OLDPC", "OLDPC",
"opencl",
"Papi", "Papi",
"Philippov", "Philippov",
"pkexec", "pkexec",

View file

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

View file

@ -21,7 +21,6 @@ import styles from '@/styles/layout.module.css';
interface ConfigFileManagerProps { interface ConfigFileManagerProps {
configFiles: ConfigFile[]; configFiles: ConfigFile[];
selectedFile: string | null; selectedFile: string | null;
hasUnsavedChanges: boolean;
onFileSelection: (fileName: string) => Promise<void>; onFileSelection: (fileName: string) => Promise<void>;
onCreateNewConfig: (configName: string) => Promise<void>; onCreateNewConfig: (configName: string) => Promise<void>;
onSaveConfig: () => void; onSaveConfig: () => void;
@ -64,7 +63,6 @@ SelectItem.displayName = 'SelectItem';
export const ConfigFileManager = ({ export const ConfigFileManager = ({
configFiles, configFiles,
selectedFile, selectedFile,
hasUnsavedChanges,
onFileSelection, onFileSelection,
onCreateNewConfig, onCreateNewConfig,
onSaveConfig, 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 ( return (
<> <>
<Stack gap="xs"> <Stack gap="xs">
@ -127,11 +116,7 @@ export const ConfigFileManager = ({
<div className={styles.flex1}> <div className={styles.flex1}>
<Select <Select
placeholder="Select a configuration file" placeholder="Select a configuration file"
value={ value={selectedFile}
selectedFile === null && hasUnsavedChanges
? '__new__'
: selectedFile
}
onChange={(value: string | null) => { onChange={(value: string | null) => {
if (value === '__new__') { if (value === '__new__') {
return; return;
@ -156,24 +141,18 @@ export const ConfigFileManager = ({
/> />
</div> </div>
<Button <Button
variant={ variant="light"
selectedFile === null && hasUnsavedChanges ? 'filled' : 'light'
}
leftSection={<Plus size={14} />} leftSection={<Plus size={14} />}
size="sm" size="sm"
disabled={selectedFile === null && hasUnsavedChanges}
onClick={() => handleOpenConfigModal()} onClick={() => handleOpenConfigModal()}
> >
{selectedFile === null && hasUnsavedChanges New
? 'Creating New...'
: 'New'}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
leftSection={<Save size={14} />} leftSection={<Save size={14} />}
size="sm" size="sm"
disabled={!hasUnsavedChanges}
onClick={() => { onClick={() => {
if (selectedFile) { if (selectedFile) {
onSaveConfig(); 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 { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css'; import styles from '@/styles/layout.module.css';
interface AdvancedTabProps { export const AdvancedTab = () => {
additionalArguments: string; const {
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 = ({
additionalArguments, additionalArguments,
noshift, noshift,
flashattention, flashattention,
@ -30,14 +14,14 @@ export const AdvancedTab = ({
lowvram, lowvram,
quantmatmul, quantmatmul,
backend, backend,
onAdditionalArgumentsChange, handleAdditionalArgumentsChange,
onNoshiftChange, handleNoshiftChange,
onFlashattentionChange, handleFlashattentionChange,
onNoavx2Change, handleNoavx2Change,
onFailsafeChange, handleFailsafeChange,
onLowvramChange, handleLowvramChange,
onQuantmatmulChange, handleQuantmatmulChange,
}: AdvancedTabProps) => { } = useLaunchConfig();
const [backendSupport, setBackendSupport] = useState<{ const [backendSupport, setBackendSupport] = useState<{
noavx2: boolean; noavx2: boolean;
failsafe: boolean; failsafe: boolean;
@ -87,7 +71,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={!noshift} checked={!noshift}
onChange={(event) => onChange={(event) =>
onNoshiftChange(!event.currentTarget.checked) handleNoshiftChange(!event.currentTarget.checked)
} }
label="Context Shift" label="Context Shift"
/> />
@ -100,7 +84,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={noshift} checked={noshift}
onChange={(event) => onChange={(event) =>
onNoshiftChange(event.currentTarget.checked) handleNoshiftChange(event.currentTarget.checked)
} }
label="No Shift" label="No Shift"
/> />
@ -115,7 +99,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={flashattention} checked={flashattention}
onChange={(event) => onChange={(event) =>
onFlashattentionChange(event.currentTarget.checked) handleFlashattentionChange(event.currentTarget.checked)
} }
label="Flash Attention" label="Flash Attention"
/> />
@ -128,7 +112,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={lowvram} checked={lowvram}
onChange={(event) => onChange={(event) =>
onLowvramChange(event.currentTarget.checked) handleLowvramChange(event.currentTarget.checked)
} }
label="Low VRAM" label="Low VRAM"
disabled={backend !== 'cuda' && backend !== 'rocm'} disabled={backend !== 'cuda' && backend !== 'rocm'}
@ -150,7 +134,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={quantmatmul} checked={quantmatmul}
onChange={(event) => onChange={(event) =>
onQuantmatmulChange(event.currentTarget.checked) handleQuantmatmulChange(event.currentTarget.checked)
} }
label="QuantMatMul" label="QuantMatMul"
disabled={backend !== 'cuda' && backend !== 'rocm'} disabled={backend !== 'cuda' && backend !== 'rocm'}
@ -181,7 +165,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={noavx2} checked={noavx2}
onChange={(event) => onChange={(event) =>
onNoavx2Change(event.currentTarget.checked) handleNoavx2Change(event.currentTarget.checked)
} }
label="Disable AVX2" label="Disable AVX2"
disabled={isLoading || !backendSupport?.noavx2} disabled={isLoading || !backendSupport?.noavx2}
@ -201,7 +185,7 @@ export const AdvancedTab = ({
<Checkbox <Checkbox
checked={failsafe} checked={failsafe}
onChange={(event) => onChange={(event) =>
onFailsafeChange(event.currentTarget.checked) handleFailsafeChange(event.currentTarget.checked)
} }
label="Failsafe" label="Failsafe"
disabled={isLoading || !backendSupport?.failsafe} disabled={isLoading || !backendSupport?.failsafe}
@ -230,7 +214,7 @@ export const AdvancedTab = ({
placeholder="Additional command line arguments" placeholder="Additional command line arguments"
value={additionalArguments} value={additionalArguments}
onChange={(event) => onChange={(event) =>
onAdditionalArgumentsChange(event.currentTarget.value) handleAdditionalArgumentsChange(event.currentTarget.value)
} }
/> />
</div> </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 { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem'; import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
interface BackendSelectorProps { interface BackendSelectorProps {
backend: string;
onBackendChange: (backend: string) => void;
gpuDevice?: number;
onGpuDeviceChange?: (device: number) => void;
noavx2?: boolean;
failsafe?: boolean;
onWarningsChange?: ( onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }> warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void; ) => void;
onBackendsReady?: () => void; onBackendsReady?: () => void;
gpuLayers?: number;
autoGpuLayers?: boolean;
onGpuLayersChange?: (layers: number) => void;
onAutoGpuLayersChange?: (auto: boolean) => void;
} }
export const BackendSelector = ({ export const BackendSelector = ({
backend,
onBackendChange,
gpuDevice = 0,
onGpuDeviceChange,
noavx2 = false,
failsafe = false,
onWarningsChange, onWarningsChange,
onBackendsReady, onBackendsReady,
gpuLayers = 0,
autoGpuLayers = false,
onGpuLayersChange,
onAutoGpuLayersChange,
}: BackendSelectorProps) => { }: BackendSelectorProps) => {
const {
backend,
gpuDevice,
noavx2,
failsafe,
gpuLayers,
autoGpuLayers,
handleBackendChange,
handleGpuDeviceChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
} = useLaunchConfig();
const [availableBackends, setAvailableBackends] = useState< const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }> Array<{ value: string; label: string; devices?: string[] }>
>([]); >([]);
@ -44,10 +38,6 @@ export const BackendSelector = ({
const hasInitialized = useRef(false); const hasInitialized = useRef(false);
useEffect(() => { useEffect(() => {
if (hasInitialized.current) return;
let timeoutId: number;
const loadBackends = async () => { const loadBackends = async () => {
try { try {
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] = const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
@ -83,16 +73,8 @@ export const BackendSelector = ({
setAvailableBackends(backends); setAvailableBackends(backends);
hasInitialized.current = true; hasInitialized.current = true;
if (backends.length > 0 && !backend) {
timeoutId = window.setTimeout(() => {
onBackendChange(backends[0].value);
}, 10);
}
if (onBackendsReady) { if (onBackendsReady) {
window.setTimeout(() => {
onBackendsReady(); onBackendsReady();
}, 100);
} }
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
@ -107,14 +89,10 @@ export const BackendSelector = ({
} }
}; };
if (!hasInitialized.current) {
loadBackends(); loadBackends();
return () => {
if (timeoutId) {
window.clearTimeout(timeoutId);
} }
}; }, [onBackendsReady]);
}, [backend, onBackendChange, onBackendsReady]);
useEffect(() => { useEffect(() => {
if (!onWarningsChange) return; if (!onWarningsChange) return;
@ -130,7 +108,7 @@ export const BackendSelector = ({
warnings.push({ warnings.push({
type: 'warning', type: 'warning',
message: 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({ warnings.push({
type: 'warning', type: 'warning',
message: 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} value={backend}
onChange={(value) => { onChange={(value) => {
if (value) { if (value) {
onBackendChange(value); handleBackendChange(value);
} }
}} }}
data={availableBackends.map((b) => ({ data={availableBackends.map((b) => ({
@ -186,11 +164,6 @@ export const BackendSelector = ({
label: b.label, label: b.label,
}))} }))}
disabled={availableBackends.length === 0} disabled={availableBackends.length === 0}
comboboxProps={{
middlewares: {
flip: false,
},
}}
renderOption={({ option }) => { renderOption={({ option }) => {
const backendData = availableBackends.find( const backendData = availableBackends.find(
(b) => b.value === option.value (b) => b.value === option.value
@ -213,14 +186,13 @@ export const BackendSelector = ({
return ( return (
isGpuBackend && isGpuBackend &&
onGpuDeviceChange &&
hasMultipleDevices && ( hasMultipleDevices && (
<Select <Select
label="GPU Device" label="GPU Device"
placeholder="Select GPU device" placeholder="Select GPU device"
value={gpuDevice.toString()} value={gpuDevice.toString()}
onChange={(value) => onChange={(value) =>
value && onGpuDeviceChange(parseInt(value, 10)) value && handleGpuDeviceChange(parseInt(value, 10))
} }
data={selectedBackend.devices!.map((device, index) => ({ data={selectedBackend.devices!.map((device, index) => ({
value: index.toString(), value: index.toString(),
@ -233,7 +205,6 @@ export const BackendSelector = ({
})()} })()}
</div> </div>
{onGpuLayersChange && onAutoGpuLayersChange && (
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
@ -245,7 +216,7 @@ export const BackendSelector = ({
<TextInput <TextInput
value={gpuLayers.toString()} value={gpuLayers.toString()}
onChange={(event) => onChange={(event) =>
onGpuLayersChange(Number(event.target.value) || 0) handleGpuLayersChange(Number(event.target.value) || 0)
} }
type="number" type="number"
min={0} min={0}
@ -260,7 +231,7 @@ export const BackendSelector = ({
label="Auto" label="Auto"
checked={autoGpuLayers} checked={autoGpuLayers}
onChange={(event) => onChange={(event) =>
onAutoGpuLayersChange(event.currentTarget.checked) handleAutoGpuLayersChange(event.currentTarget.checked)
} }
size="sm" size="sm"
/> />
@ -268,7 +239,6 @@ export const BackendSelector = ({
</Group> </Group>
</Group> </Group>
</div> </div>
)}
</Group> </Group>
</div> </div>
); );

View file

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

View file

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

View file

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

View file

@ -1,10 +1,9 @@
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core'; import { Card, Container, Stack, Tabs, Group, Button } 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 { useTrackedConfigHandlers } from '@/hooks/useTrackedConfigHandlers';
import { useLaunchLogic } from '@/hooks/useLaunchLogic'; import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings'; 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 { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
import { NetworkTab } from '@/components/screens/Launch/NetworkTab'; import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab'; import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
@ -25,7 +24,6 @@ export const LaunchScreen = ({
const [selectedFile, setSelectedFile] = useState<string | null>(null); const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [, setInstallDir] = useState<string>(''); const [, setInstallDir] = useState<string>('');
const [activeTab, setActiveTab] = useState<string | null>('general'); const [activeTab, setActiveTab] = useState<string | null>('general');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [warnings, setWarnings] = useState< const [warnings, setWarnings] = useState<
Array<{ type: 'warning' | 'info'; message: string }> Array<{ type: 'warning' | 'info'; message: string }>
>([]); >([]);
@ -51,6 +49,7 @@ export const LaunchScreen = ({
quantmatmul, quantmatmul,
backend, backend,
gpuDevice, gpuDevice,
gpuPlatform,
sdmodel, sdmodel,
sdt5xxl, sdt5xxl,
sdclipl, sdclipl,
@ -59,78 +58,9 @@ export const LaunchScreen = ({
sdvae, sdvae,
sdlora, sdlora,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadSavedSettings,
loadConfigFromFile, 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(); } = 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({ const { isLaunching, handleLaunch } = useLaunchLogic({
modelPath, modelPath,
sdmodel, sdmodel,
@ -156,13 +86,11 @@ export const LaunchScreen = ({
setSelectedFile(files[0].name); setSelectedFile(files[0].name);
} }
await loadSavedSettings(); const loadedConfigFileName = await loadConfigFromFile(files, savedConfig);
if (loadedConfigFileName && !selectedFile) {
const currentSelectedFile = await loadConfigFromFile(files, savedConfig); setSelectedFile(loadedConfigFileName);
if (currentSelectedFile && !selectedFile) {
setSelectedFile(currentSelectedFile);
} }
}, [selectedFile, loadSavedSettings, loadConfigFromFile]); }, [selectedFile, loadConfigFromFile]);
const handleFileSelection = async (fileName: string) => { const handleFileSelection = async (fileName: string) => {
setSelectedFile(fileName); setSelectedFile(fileName);
@ -172,14 +100,19 @@ export const LaunchScreen = ({
if (selectedConfig) { if (selectedConfig) {
await parseAndApplyConfigFile(selectedConfig.path); 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, gpulayers: gpuLayers,
contextsize: contextSize, contextsize: contextSize,
model_param: modelPath, model: modelPath,
port, port,
host, host,
multiuser: multiuser ? 1 : 0, multiuser: multiuser ? 1 : 0,
@ -193,14 +126,15 @@ export const LaunchScreen = ({
failsafe, failsafe,
usecuda: backend === 'cuda' || backend === 'rocm', usecuda: backend === 'cuda' || backend === 'rocm',
usevulkan: backend === 'vulkan', usevulkan: backend === 'vulkan',
useclblast: backend === 'clblast', useclblast: useclblastValue,
sdmodel, sdmodel,
sdt5xxl, sdt5xxl,
sdclipl, sdclipl,
sdclipg, sdclipg,
sdphotomaker, sdphotomaker,
sdvae, sdvae,
}); };
};
const handleCreateNewConfig = async (configName: string) => { const handleCreateNewConfig = async (configName: string) => {
try { try {
@ -214,7 +148,6 @@ export const LaunchScreen = ({
const newFileName = `${configName}.kcpps`; const newFileName = `${configName}.kcpps`;
setSelectedFile(newFileName); setSelectedFile(newFileName);
await window.electronAPI.kobold.setSelectedConfig(newFileName); await window.electronAPI.kobold.setSelectedConfig(newFileName);
setHasUnsavedChanges(false);
} else { } else {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
'Failed to create new configuration', 'Failed to create new configuration',
@ -246,9 +179,7 @@ export const LaunchScreen = ({
buildConfigData() buildConfigData()
); );
if (success) { if (!success) {
setHasUnsavedChanges(false);
} else {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
'Failed to save configuration', 'Failed to save configuration',
new Error('Save operation failed') new Error('Save operation failed')
@ -318,7 +249,6 @@ export const LaunchScreen = ({
<ConfigFileManager <ConfigFileManager
configFiles={configFiles} configFiles={configFiles}
selectedFile={selectedFile} selectedFile={selectedFile}
hasUnsavedChanges={hasUnsavedChanges}
onFileSelection={handleFileSelection} onFileSelection={handleFileSelection}
onCreateNewConfig={handleCreateNewConfig} onCreateNewConfig={handleCreateNewConfig}
onSaveConfig={handleSaveConfig} onSaveConfig={handleSaveConfig}
@ -335,138 +265,19 @@ export const LaunchScreen = ({
<div> <div>
<Tabs.Panel value="general" pt="md"> <Tabs.Panel value="general" pt="md">
<GeneralTab <GeneralTab onWarningsChange={setWarnings} />
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}
/>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="advanced" pt="md"> <Tabs.Panel value="advanced" pt="md">
<AdvancedTab <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
}
/>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="network" pt="md"> <Tabs.Panel value="network" pt="md">
<NetworkTab <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
}
/>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="image" pt="md"> <Tabs.Panel value="image" pt="md">
<ImageGenerationTab <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}
/>
</Tabs.Panel> </Tabs.Panel>
</div> </div>
</Tabs> </Tabs>

View file

View file

@ -1,534 +1,90 @@
import { useState, useCallback } from 'react'; import { useLaunchConfigStore } from '@/stores/launchConfigStore';
import type { ConfigFile } from '@/types';
import { import {
getPresetByName, IMAGE_MODEL_PRESETS,
type ImageModelPreset, type ImageModelPreset,
} from '@/utils/imageModelPresets'; } from '@/utils/imageModelPresets';
import {
DEFAULT_CONTEXT_SIZE,
DEFAULT_MODEL_URL,
DEFAULT_HOST,
} from '@/constants';
export const useLaunchConfig = () => { export const useLaunchConfig = () => {
const [gpuLayers, setGpuLayers] = useState<number>(0); const state = useLaunchConfigStore();
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]
);
return { return {
gpuLayers, gpuLayers: state.gpuLayers,
autoGpuLayers, autoGpuLayers: state.autoGpuLayers,
contextSize, contextSize: state.contextSize,
modelPath, modelPath: state.modelPath,
additionalArguments, additionalArguments: state.additionalArguments,
port, port: state.port,
host, host: state.host,
multiuser, multiuser: state.multiuser,
multiplayer, multiplayer: state.multiplayer,
remotetunnel, remotetunnel: state.remotetunnel,
nocertify, nocertify: state.nocertify,
websearch, websearch: state.websearch,
noshift, noshift: state.noshift,
flashattention, flashattention: state.flashattention,
noavx2, noavx2: state.noavx2,
failsafe, failsafe: state.failsafe,
lowvram, lowvram: state.lowvram,
quantmatmul, quantmatmul: state.quantmatmul,
backend, backend: state.backend,
gpuDevice, gpuDevice: state.gpuDevice,
sdmodel, gpuPlatform: state.gpuPlatform,
sdt5xxl, sdmodel: state.sdmodel,
sdclipl, sdt5xxl: state.sdt5xxl,
sdclipg, sdclipl: state.sdclipl,
sdphotomaker, sdclipg: state.sdclipg,
sdvae, sdphotomaker: state.sdphotomaker,
sdlora, sdvae: state.sdvae,
sdlora: state.sdlora,
parseAndApplyConfigFile, handleGpuLayersChange: state.setGpuLayers,
loadSavedSettings, handleAutoGpuLayersChange: state.setAutoGpuLayers,
loadConfigFromFile, handleContextSizeChangeWithStep: state.contextSizeChangeWithStep,
handleGpuLayersChange, handleModelPathChange: state.setModelPath,
handleAutoGpuLayersChange, handleAdditionalArgumentsChange: state.setAdditionalArguments,
handleContextSizeChangeWithStep, handlePortChange: state.setPort,
handleModelPathChange, handleHostChange: state.setHost,
handleSelectModelFile, handleMultiuserChange: state.setMultiuser,
handleAdditionalArgumentsChange, handleMultiplayerChange: state.setMultiplayer,
handlePortChange, handleRemotetunnelChange: state.setRemotetunnel,
handleHostChange, handleNocertifyChange: state.setNocertify,
handleMultiuserChange, handleWebsearchChange: state.setWebsearch,
handleMultiplayerChange, handleNoshiftChange: state.setNoshift,
handleRemotetunnelChange, handleFlashattentionChange: state.setFlashattention,
handleNocertifyChange, handleNoavx2Change: state.setNoavx2,
handleWebsearchChange, handleFailsafeChange: state.setFailsafe,
handleNoshiftChange, handleLowvramChange: state.setLowvram,
handleFlashattentionChange, handleQuantmatmulChange: state.setQuantmatmul,
handleNoavx2Change, handleBackendChange: state.setBackend,
handleFailsafeChange, handleGpuDeviceChange: state.setGpuDevice,
handleLowvramChange, handleGpuPlatformChange: state.setGpuPlatform,
handleQuantmatmulChange, handleSdmodelChange: state.setSdmodel,
handleBackendChange, handleSdt5xxlChange: state.setSdt5xxl,
handleGpuDeviceChange, handleSdcliplChange: state.setSdclipl,
handleSdmodelChange, handleSdclipgChange: state.setSdclipg,
handleSelectSdmodelFile, handleSdphotomakerChange: state.setSdphotomaker,
handleSdt5xxlChange, handleSdvaeChange: state.setSdvae,
handleSelectSdt5xxlFile, handleSdloraChange: state.setSdlora,
handleSdcliplChange,
handleSelectSdcliplFile, parseAndApplyConfigFile: state.parseAndApplyConfigFile,
handleSdclipgChange, loadConfigFromFile: state.loadConfigFromFile,
handleSelectSdclipgFile, handleSelectModelFile: state.selectModelFile,
handleSdphotomakerChange, handleImageModelPresetChange: state.applyImageModelPreset,
handleSelectSdphotomakerFile, handleApplyPreset: (presetName: string) => {
handleSdvaeChange, const preset = IMAGE_MODEL_PRESETS.find(
handleSelectSdvaeFile, (p: ImageModelPreset) => p.name === presetName
handleSdloraChange, );
handleSelectSdloraFile, if (preset) {
applyImageModelPreset, state.applyImageModelPreset(preset);
handleApplyPreset, }
},
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 { useState, useCallback } from 'react';
import { parseCLBlastDevice } from '@/utils';
interface UseLaunchLogicProps { interface UseLaunchLogicProps {
modelPath: string; modelPath: string;
@ -22,7 +23,7 @@ interface LaunchArgs {
flashattention: boolean; flashattention: boolean;
backend: string; backend: string;
lowvram: boolean; lowvram: boolean;
gpuDevice: number; gpuDevice: number | string;
quantmatmul: boolean; quantmatmul: boolean;
additionalArguments: string; additionalArguments: string;
sdt5xxl: string; sdt5xxl: string;
@ -116,13 +117,33 @@ const buildBackendArgs = (launchArgs: LaunchArgs): string[] => {
if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') { if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') {
const cudaArgs = ['--usecuda']; const cudaArgs = ['--usecuda'];
cudaArgs.push(launchArgs.lowvram ? 'lowvram' : 'normal'); 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'); cudaArgs.push(launchArgs.quantmatmul ? 'mmq' : 'nommq');
args.push(...cudaArgs); args.push(...cudaArgs);
} else if (launchArgs.backend === 'vulkan') { } else if (launchArgs.backend === 'vulkan') {
args.push('--usevulkan'); args.push('--usevulkan');
} else if (launchArgs.backend === 'clblast') { } 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; handleLowvramChange: (enabled: boolean) => void;
handleQuantmatmulChange: (enabled: boolean) => void; handleQuantmatmulChange: (enabled: boolean) => void;
handleBackendChange: (backend: string) => void; handleBackendChange: (backend: string) => void;
handleGpuDeviceChange: (device: number) => void;
handleGpuPlatformChange: (platform: number) => void;
handleSdmodelChange: (path: string) => void; handleSdmodelChange: (path: string) => void;
handleSdt5xxlChange: (path: string) => void; handleSdt5xxlChange: (path: string) => void;
handleSdcliplChange: (path: string) => void; handleSdcliplChange: (path: string) => void;
@ -58,6 +60,8 @@ export const useTrackedConfigHandlers = ({
handleLowvramChange, handleLowvramChange,
handleQuantmatmulChange, handleQuantmatmulChange,
handleBackendChange, handleBackendChange,
handleGpuDeviceChange,
handleGpuPlatformChange,
handleSdmodelChange, handleSdmodelChange,
handleSdt5xxlChange, handleSdt5xxlChange,
handleSdcliplChange, handleSdcliplChange,
@ -144,6 +148,14 @@ export const useTrackedConfigHandlers = ({
handleBackendChange, handleBackendChange,
setHasUnsavedChanges setHasUnsavedChanges
), ),
handleGpuDeviceChangeWithTracking: createChangeTracker(
handleGpuDeviceChange,
setHasUnsavedChanges
),
handleGpuPlatformChangeWithTracking: createChangeTracker(
handleGpuPlatformChange,
setHasUnsavedChanges
),
handleSdmodelChangeWithTracking: createChangeTracker( handleSdmodelChangeWithTracking: createChangeTracker(
handleSdmodelChange, handleSdmodelChange,
setHasUnsavedChanges setHasUnsavedChanges

View file

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

View file

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

View file

@ -1,5 +1,6 @@
/* eslint-disable no-comments/disallowComments */ /* eslint-disable no-comments/disallowComments */
import si from 'systeminformation'; import si from 'systeminformation';
import * as openclInfo from 'opencl-info';
import { shortenDeviceName } from '@/utils'; import { shortenDeviceName } from '@/utils';
import type { import type {
CPUCapabilities, CPUCapabilities,
@ -343,74 +344,35 @@ export class HardwareService {
devices: string[]; devices: string[];
}> { }> {
try { try {
const { spawn } = await import('child_process'); const platforms = openclInfo.getPlatformInfo();
const clinfo = spawn('clinfo', ['--json'], { timeout: 5000 });
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[] = []; const devices: string[] = [];
if (data.platforms) { for (
for (const platform of data.platforms) { let platformIndex = 0;
platformIndex < platforms.length;
platformIndex++
) {
const platform = platforms[platformIndex];
if (platform.devices) { if (platform.devices) {
for (const device of platform.devices) { for (
if (device.name && device.type !== 'CPU') { let deviceIndex = 0;
devices.push(shortenDeviceName(device.name)); 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, supported: devices.length > 0,
devices, 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 { } catch {
return { supported: false, devices: [] }; return { supported: false, devices: [] };
} }

View file

@ -55,7 +55,7 @@ const koboldAPI: KoboldAPI = {
configData: { configData: {
gpulayers?: number; gpulayers?: number;
contextsize?: number; contextsize?: number;
model_param?: string; model?: string;
port?: number; port?: number;
host?: string; host?: string;
multiuser?: number; multiuser?: number;
@ -69,7 +69,7 @@ const koboldAPI: KoboldAPI = {
failsafe?: boolean; failsafe?: boolean;
usecuda?: boolean; usecuda?: boolean;
usevulkan?: boolean; usevulkan?: boolean;
useclblast?: boolean; useclblast?: [number, number] | boolean;
sdmodel?: string; sdmodel?: string;
sdt5xxl?: string; sdt5xxl?: string;
sdclipl?: 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: { configData: {
gpulayers?: number; gpulayers?: number;
contextsize?: number; contextsize?: number;
model_param?: string; model?: string;
port?: number; port?: number;
host?: string; host?: string;
multiuser?: number; multiuser?: number;
@ -124,7 +124,7 @@ export interface KoboldAPI {
failsafe?: boolean; failsafe?: boolean;
usecuda?: boolean; usecuda?: boolean;
usevulkan?: boolean; usevulkan?: boolean;
useclblast?: boolean; useclblast?: [number, number] | boolean;
sdmodel?: string; sdmodel?: string;
sdt5xxl?: string; sdt5xxl?: string;
sdclipl?: string; sdclipl?: string;
@ -139,7 +139,7 @@ export interface KoboldAPI {
parseConfigFile: (filePath: string) => Promise<{ parseConfigFile: (filePath: string) => Promise<{
gpulayers?: number; gpulayers?: number;
contextsize?: number; contextsize?: number;
model_param?: string; model?: string;
[key: string]: unknown; [key: string]: unknown;
} | null>; } | null>;
selectModelFile: () => Promise<string | 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 './assets';
export * from './clblast';
export * from './downloadUtils'; export * from './downloadUtils';
export * from './fileSize'; export * from './fileSize';
export * from './hardware'; export * from './hardware';

912
yarn.lock

File diff suppressed because it is too large Load diff