unite all warning and info detection into a single useWarnings hook

This commit is contained in:
Egor 2025-08-23 14:59:06 -07:00
parent 3953b13794
commit 70245e6a1f
5 changed files with 302 additions and 236 deletions

View file

@ -3,24 +3,15 @@ import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { checkBackendWarnings } from '@/utils/backendWarnings';
interface BackendSelectorProps {
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
}
export const BackendSelector = ({
onWarningsChange,
onBackendsReady,
}: BackendSelectorProps) => {
export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
const {
backend,
gpuDevice,
noavx2,
failsafe,
gpuLayers,
autoGpuLayers,
handleBackendChange,
@ -32,10 +23,6 @@ export const BackendSelector = ({
const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }>
>([]);
const [cpuCapabilities, setCpuCapabilities] = useState<{
avx: boolean;
avx2: boolean;
} | null>(null);
const hasInitialized = useRef(false);
useEffect(() => {
@ -46,11 +33,6 @@ export const BackendSelector = ({
window.electronAPI.kobold.getAvailableBackends(),
]);
setCpuCapabilities({
avx: cpuCapabilitiesResult.avx,
avx2: cpuCapabilitiesResult.avx2,
});
const cpuBackend = backends.find((b) => b.value === 'cpu');
if (cpuBackend) {
cpuBackend.devices = cpuCapabilitiesResult.devices;
@ -87,38 +69,6 @@ export const BackendSelector = ({
return cleanup;
}, [onBackendsReady]);
useEffect(() => {
if (!onWarningsChange) return;
const updateWarnings = async () => {
try {
const warnings = await checkBackendWarnings({
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
});
onWarningsChange(warnings);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
onWarningsChange([]);
}
};
updateWarnings();
}, [
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
onWarningsChange,
]);
return (
<div>
<Group justify="space-between" align="flex-start" mb="xs">

View file

@ -7,16 +7,10 @@ import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import styles from '@/styles/layout.module.css';
interface GeneralTabProps {
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
}
export const GeneralTab = ({
onWarningsChange,
onBackendsReady,
}: GeneralTabProps) => {
export const GeneralTab = ({ onBackendsReady }: GeneralTabProps) => {
const {
modelPath,
contextSize,
@ -50,10 +44,7 @@ export const GeneralTab = ({
return (
<Stack gap="md">
<BackendSelector
onWarningsChange={onWarningsChange}
onBackendsReady={onBackendsReady}
/>
<BackendSelector onBackendsReady={onBackendsReady} />
<div>
<Text size="sm" fw={500} mb="xs">

View file

@ -25,9 +25,6 @@ export const LaunchScreen = ({
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [, setInstallDir] = useState<string>('');
const [activeTab, setActiveTab] = useState<string | null>('general');
const [warnings, setWarnings] = useState<
Array<{ type: 'warning' | 'info'; message: string }>
>([]);
const [configLoaded, setConfigLoaded] = useState<boolean>(false);
const {
@ -73,7 +70,14 @@ export const LaunchScreen = ({
onLaunchModeChange,
});
const combinedWarnings = useWarnings({ modelPath, sdmodel, warnings });
const { warnings: combinedWarnings } = useWarnings({
modelPath,
sdmodel,
backend,
noavx2,
failsafe,
configLoaded,
});
const setHappyDefaults = useCallback(async () => {
try {
@ -302,7 +306,7 @@ export const LaunchScreen = ({
<div>
<Tabs.Panel value="general" pt="md">
<GeneralTab onWarningsChange={setWarnings} />
<GeneralTab />
</Tabs.Panel>
<Tabs.Panel value="advanced" pt="md">

View file

@ -1,41 +1,300 @@
import { useMemo } from 'react';
import { useMemo, useEffect, useState, useCallback } from 'react';
export interface Warning {
type: 'warning' | 'info';
message: string;
}
interface UseWarningsProps {
modelPath: string;
sdmodel: string;
warnings: Array<{ type: 'warning' | 'info'; message: string }>;
backend?: string;
noavx2?: boolean;
failsafe?: boolean;
configLoaded?: boolean;
}
const checkModelWarnings = (
modelPath: string,
sdmodel: string,
configLoaded: boolean
): Warning[] => {
const hasTextModel = modelPath?.trim() !== '';
const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel;
const showNoModelWarning = !hasTextModel && !hasImageModel && configLoaded;
const warnings: Warning[] = [];
if (showModelPriorityWarning) {
warnings.push({
type: 'warning',
message:
'Both text and image generation models are selected. The image generation model will take priority and be used for launch.',
});
}
if (showNoModelWarning) {
warnings.push({
type: 'info',
message:
'Select a model in the General or Image Generation tab to enable launch.',
});
}
return warnings;
};
interface BackendSupport {
cuda: boolean;
rocm: boolean;
}
interface GpuCapabilities {
cuda: { supported: boolean };
rocm: { supported: boolean };
}
interface GpuInfo {
hasNVIDIA: boolean;
hasAMD: boolean;
}
const checkGpuWarnings = async (
backendSupport: BackendSupport,
gpuCapabilities: GpuCapabilities,
gpuInfo: GpuInfo
): Promise<Warning[]> => {
const warnings: Warning[] = [];
if (
backendSupport.cuda &&
!gpuCapabilities.cuda.supported &&
gpuInfo.hasNVIDIA
) {
warnings.push({
type: 'warning',
message:
'Your KoboldCpp binary supports CUDA and you have an NVIDIA GPU, but CUDA runtime is not detected on your system.',
});
}
if (
backendSupport.rocm &&
!gpuCapabilities.rocm.supported &&
gpuInfo.hasAMD
) {
warnings.push({
type: 'warning',
message:
'Your KoboldCpp binary supports ROCm and you have an AMD GPU, but ROCm runtime is not detected on your system.',
});
}
return warnings;
};
const checkVramWarnings = async (backend: string): Promise<Warning[]> => {
const warnings: Warning[] = [];
const isGpuBackend = ['cuda', 'rocm', 'vulkan', 'clblast'].includes(backend);
if (isGpuBackend) {
try {
const gpuMemoryInfo = await window.electronAPI.kobold.detectGPUMemory();
const lowVramGpus = gpuMemoryInfo.filter(
(gpu) =>
typeof gpu.totalMemoryMB === 'number' && gpu.totalMemoryMB < 8192
);
if (lowVramGpus.length > 0) {
warnings.push({
type: 'warning',
message: `Low VRAM detected (${lowVramGpus
.map(
(gpu) =>
`${gpu.deviceName}: ${(gpu.totalMemoryMB! / 1024).toFixed(1)}GB`
)
.join(
', '
)}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`,
});
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to detect GPU memory:',
error as Error
);
}
}
return warnings;
};
const checkCpuWarnings = (
backend: string,
cpuCapabilities: { avx: boolean; avx2: boolean },
noavx2: boolean,
failsafe: boolean,
availableBackends: Array<{ value: string; label: string; devices?: string[] }>
): Warning[] => {
const warnings: Warning[] = [];
if (backend !== 'cpu') {
return warnings;
}
if (!cpuCapabilities.avx2 && !noavx2) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option on the Advanced tab to avoid crashes.',
});
}
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option on the Advanced tab to avoid crashes.',
});
}
if (
availableBackends.length > 0 &&
availableBackends.some((b) => b.value === 'cpu')
) {
warnings.push({
type: 'info',
message:
"LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.",
});
}
return warnings;
};
const checkBackendWarnings = async (params?: {
backend: string;
cpuCapabilities: {
avx: boolean;
avx2: boolean;
} | null;
noavx2: boolean;
failsafe: boolean;
availableBackends: Array<{
value: string;
label: string;
devices?: string[];
}>;
}): Promise<Warning[]> => {
const warnings: Warning[] = [];
try {
const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([
window.electronAPI.kobold.detectBackendSupport(),
window.electronAPI.kobold.detectGPUCapabilities(),
window.electronAPI.kobold.detectGPU(),
]);
if (!backendSupport) {
return warnings;
}
const gpuWarnings = await checkGpuWarnings(
backendSupport,
gpuCapabilities,
gpuInfo
);
warnings.push(...gpuWarnings);
if (params) {
const { backend, cpuCapabilities, noavx2, failsafe, availableBackends } =
params;
const vramWarnings = await checkVramWarnings(backend);
warnings.push(...vramWarnings);
if (cpuCapabilities) {
const cpuWarnings = checkCpuWarnings(
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends
);
warnings.push(...cpuWarnings);
}
}
return warnings;
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
return warnings;
}
};
export const useWarnings = ({
modelPath,
sdmodel,
warnings,
}: UseWarningsProps) =>
useMemo(() => {
const hasTextModel = modelPath?.trim() !== '';
const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel;
const showNoModelWarning = !hasTextModel && !hasImageModel;
backend,
noavx2 = false,
failsafe = false,
configLoaded = false,
}: UseWarningsProps) => {
const [backendWarnings, setBackendWarnings] = useState<Warning[]>([]);
return [
...warnings,
...(showModelPriorityWarning
? [
{
type: 'warning' as const,
message:
'Both text and image generation models are selected. The image generation model will take priority and be used for launch.',
},
]
: []),
...(showNoModelWarning
? [
{
type: 'info' as const,
message:
'Select a model in the General or Image Generation tab to enable launch.',
},
]
: []),
];
}, [modelPath, sdmodel, warnings]);
const modelWarnings = useMemo(
() => checkModelWarnings(modelPath, sdmodel, configLoaded),
[modelPath, sdmodel, configLoaded]
);
const updateBackendWarnings = useCallback(async () => {
if (!backend) {
setBackendWarnings([]);
return;
}
try {
const [cpuCapabilitiesResult, availableBackends] = await Promise.all([
window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.getAvailableBackends(),
]);
const cpuCapabilities = {
avx: cpuCapabilitiesResult.avx,
avx2: cpuCapabilitiesResult.avx2,
};
const warnings = await checkBackendWarnings({
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
});
setBackendWarnings(warnings);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
setBackendWarnings([]);
}
}, [backend, noavx2, failsafe]);
useEffect(() => {
updateBackendWarnings();
}, [updateBackendWarnings]);
const allWarnings = useMemo(
() => [...modelWarnings, ...backendWarnings],
[modelWarnings, backendWarnings]
);
return {
warnings: allWarnings,
};
};

View file

@ -1,138 +0,0 @@
export interface BackendWarning {
type: 'warning' | 'info';
message: string;
}
interface WarningParams {
backend: string;
cpuCapabilities: {
avx: boolean;
avx2: boolean;
} | null;
noavx2: boolean;
failsafe: boolean;
availableBackends: Array<{
value: string;
label: string;
devices?: string[];
}>;
}
export const checkBackendWarnings = async (
params?: WarningParams
// eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<BackendWarning[]> => {
const warnings: BackendWarning[] = [];
try {
const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([
window.electronAPI.kobold.detectBackendSupport(),
window.electronAPI.kobold.detectGPUCapabilities(),
window.electronAPI.kobold.detectGPU(),
]);
if (!backendSupport) {
return warnings;
}
if (
backendSupport.cuda &&
!gpuCapabilities.cuda.supported &&
gpuInfo.hasNVIDIA
) {
warnings.push({
type: 'warning',
message:
'Your KoboldCpp binary supports CUDA and you have an NVIDIA GPU, but CUDA runtime is not detected on your system.',
});
}
if (
backendSupport.rocm &&
!gpuCapabilities.rocm.supported &&
gpuInfo.hasAMD
) {
warnings.push({
type: 'warning',
message:
'Your KoboldCpp binary supports ROCm and you have an AMD GPU, but ROCm runtime is not detected on your system.',
});
}
if (params) {
const { backend, cpuCapabilities, noavx2, failsafe, availableBackends } =
params;
const isGpuBackend = ['cuda', 'rocm', 'vulkan', 'clblast'].includes(
backend
);
if (isGpuBackend) {
try {
const gpuMemoryInfo =
await window.electronAPI.kobold.detectGPUMemory();
const lowVramGpus = gpuMemoryInfo.filter(
(gpu) =>
typeof gpu.totalMemoryMB === 'number' && gpu.totalMemoryMB < 8192
);
if (lowVramGpus.length > 0) {
warnings.push({
type: 'warning',
message: `Low VRAM detected (${lowVramGpus
.map(
(gpu) =>
`${gpu.deviceName}: ${(gpu.totalMemoryMB! / 1024).toFixed(1)}GB`
)
.join(
', '
)}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`,
});
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to detect GPU memory:',
error as Error
);
}
}
if (backend === 'cpu' && cpuCapabilities) {
if (!cpuCapabilities.avx2 && !noavx2) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option on the Advanced tab to avoid crashes.',
});
}
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option on the Advanced tab to avoid crashes.',
});
}
if (
availableBackends.length > 0 &&
availableBackends.some((b) => b.value === 'cpu')
) {
warnings.push({
type: 'info',
message:
"LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.",
});
}
}
}
return warnings;
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check backend warnings:',
error as Error
);
return warnings;
}
};