mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
display disabled backend options for available but currently hardware-unsupported backends, allow updatable binaries to be available to make current, persist skipping kcpp updates to app config
This commit is contained in:
parent
253403d45f
commit
4f49f0bad3
10 changed files with 244 additions and 153 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "friendly-kobold",
|
"name": "friendly-kobold",
|
||||||
"productName": "Friendly Kobold",
|
"productName": "Friendly Kobold",
|
||||||
"version": "0.8.0",
|
"version": "0.8.1",
|
||||||
"description": "A desktop app for running Large Language Models locally",
|
"description": "A desktop app for running Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
"@eslint/js": "^9.34.0",
|
"@eslint/js": "^9.34.0",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.11",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@types/strip-ansi": "^5.2.1",
|
"@types/strip-ansi": "^5.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
"@mantine/hooks": "^8.2.7",
|
"@mantine/hooks": "^8.2.7",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"got": "^14.4.7",
|
"got": "^14.4.7",
|
||||||
"lucide-react": "^0.541.0",
|
"lucide-react": "^0.542.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ interface DownloadCardProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
newerVersion?: string;
|
newerVersion?: string;
|
||||||
onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
onMakeCurrent?: () => void;
|
onMakeCurrent?: () => void;
|
||||||
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -51,32 +51,13 @@ export const DownloadCard = ({
|
||||||
getInitialValueInEffect: false,
|
getInitialValueInEffect: false,
|
||||||
});
|
});
|
||||||
const isDark = computedColorScheme === 'dark';
|
const isDark = computedColorScheme === 'dark';
|
||||||
const renderActionButton = () => {
|
const renderActionButtons = () => {
|
||||||
if (hasUpdate && onUpdate) {
|
const buttons = [];
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
size="xs"
|
|
||||||
onClick={onUpdate}
|
|
||||||
loading={isDownloading}
|
|
||||||
disabled={disabled}
|
|
||||||
color="orange"
|
|
||||||
leftSection={
|
|
||||||
isDownloading ? (
|
|
||||||
<Loader size="1rem" />
|
|
||||||
) : (
|
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isDownloading ? 'Updating...' : `Update to ${newerVersion}`}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isInstalled && onDownload) {
|
if (!isInstalled) {
|
||||||
return (
|
buttons.push(
|
||||||
<Button
|
<Button
|
||||||
|
key="download"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onDownload}
|
onClick={onDownload}
|
||||||
|
|
@ -96,14 +77,42 @@ export const DownloadCard = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInstalled && !isCurrent && onMakeCurrent) {
|
if (isInstalled && !isCurrent && onMakeCurrent) {
|
||||||
return (
|
buttons.push(
|
||||||
<Button variant="light" size="xs" onClick={onMakeCurrent}>
|
<Button
|
||||||
|
key="makeCurrent"
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
onClick={onMakeCurrent}
|
||||||
|
>
|
||||||
Make Current
|
Make Current
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (hasUpdate && onUpdate) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
key="update"
|
||||||
|
variant="filled"
|
||||||
|
size="xs"
|
||||||
|
onClick={onUpdate}
|
||||||
|
loading={isDownloading}
|
||||||
|
disabled={disabled}
|
||||||
|
color="orange"
|
||||||
|
leftSection={
|
||||||
|
isDownloading ? (
|
||||||
|
<Loader size="1rem" />
|
||||||
|
) : (
|
||||||
|
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isDownloading ? 'Updating...' : `Update to ${newerVersion}`}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttons.length > 0 ? <Stack gap="xs">{buttons}</Stack> : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -157,7 +166,7 @@ export const DownloadCard = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderActionButton()}
|
{renderActionButtons()}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{isDownloading && downloadProgress !== undefined && (
|
{isDownloading && downloadProgress !== undefined && (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { Stack, Text, Group, Button, Select, Badge } from '@mantine/core';
|
import { Stack, Text, Group, Button, Select, Badge } from '@mantine/core';
|
||||||
import {
|
import { useState, useCallback } from 'react';
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
forwardRef,
|
|
||||||
type ComponentPropsWithoutRef,
|
|
||||||
} from 'react';
|
|
||||||
import { Save, File, Plus, Check } from 'lucide-react';
|
import { Save, File, Plus, Check } from 'lucide-react';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
import styles from '@/styles/layout.module.css';
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
@ -19,7 +14,7 @@ interface ConfigFileManagerProps {
|
||||||
onLoadConfigFiles: () => Promise<void>;
|
onLoadConfigFiles: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
interface SelectItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
extension: string;
|
extension: string;
|
||||||
}
|
}
|
||||||
|
|
@ -35,23 +30,17 @@ const getBadgeColor = (extension: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
const SelectItem = ({ label, extension }: SelectItemProps) => (
|
||||||
({ label, extension, ...others }, ref) => (
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<div ref={ref} {...others}>
|
<Text size="sm" truncate>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
{label}
|
||||||
<Text size="sm" truncate>
|
</Text>
|
||||||
{label}
|
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
|
||||||
</Text>
|
{extension}
|
||||||
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
|
</Badge>
|
||||||
{extension}
|
</Group>
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
SelectItem.displayName = 'SelectItem';
|
|
||||||
|
|
||||||
export const ConfigFileManager = ({
|
export const ConfigFileManager = ({
|
||||||
configFiles,
|
configFiles,
|
||||||
selectedFile,
|
selectedFile,
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,38 @@
|
||||||
import { Text, Group, Badge } from '@mantine/core';
|
import { Text, Group, Badge } from '@mantine/core';
|
||||||
import { forwardRef } from 'react';
|
|
||||||
import type { ComponentPropsWithoutRef } from 'react';
|
|
||||||
|
|
||||||
interface BackendSelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
interface BackendSelectItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
devices?: string[];
|
devices?: string[];
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BackendSelectItem = forwardRef<
|
export const BackendSelectItem = ({
|
||||||
HTMLDivElement,
|
label,
|
||||||
BackendSelectItemProps
|
devices,
|
||||||
>(({ label, devices, ...others }, ref) => (
|
disabled = false,
|
||||||
<div ref={ref} {...others}>
|
}: BackendSelectItemProps) => (
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Text size="sm" truncate>
|
<Text size="sm" truncate>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
{disabled && (
|
||||||
{devices && devices.length > 0 && (
|
<Text component="span" size="xs" ml="xs">
|
||||||
<Group gap={4}>
|
(Compatible device(s) not found)
|
||||||
{devices.slice(0, 2).map((device, index) => (
|
</Text>
|
||||||
<Badge key={index} size="md" variant="light" color="blue">
|
|
||||||
{device.length > 25 ? `${device.slice(0, 25)}...` : device}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{devices.length > 2 && (
|
|
||||||
<Badge size="md" variant="light" color="gray">
|
|
||||||
+{devices.length - 2}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Text>
|
||||||
</div>
|
{devices && devices.length > 0 && (
|
||||||
));
|
<Group gap={4}>
|
||||||
|
{devices.slice(0, 2).map((device, index) => (
|
||||||
BackendSelectItem.displayName = 'BackendSelectItem';
|
<Badge key={index} size="md" variant="light" color="blue">
|
||||||
|
{device.length > 25 ? `${device.slice(0, 25)}...` : device}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{devices.length > 2 && (
|
||||||
|
<Badge size="md" variant="light" color="gray">
|
||||||
|
+{devices.length - 2}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/Backen
|
||||||
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
||||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
|
|
||||||
interface BackendSelectorProps {
|
export const BackendSelector = () => {
|
||||||
onBackendsReady?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
|
|
||||||
const {
|
const {
|
||||||
backend,
|
backend,
|
||||||
gpuLayers,
|
gpuLayers,
|
||||||
|
|
@ -20,39 +16,95 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
|
||||||
} = useLaunchConfig();
|
} = useLaunchConfig();
|
||||||
|
|
||||||
const [availableBackends, setAvailableBackends] = useState<
|
const [availableBackends, setAvailableBackends] = useState<
|
||||||
Array<{ value: string; label: string; devices?: string[] }>
|
Array<{
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
devices?: string[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadBackends = async () => {
|
const loadBackends = async () => {
|
||||||
try {
|
try {
|
||||||
const [cpuCapabilitiesResult, backends] = await Promise.all([
|
const [cpuCapabilitiesResult, binarySupport, gpuCapabilities] =
|
||||||
window.electronAPI.kobold.detectCPU(),
|
await Promise.all([
|
||||||
window.electronAPI.kobold.getAvailableBackends(),
|
window.electronAPI.kobold.detectCPU(),
|
||||||
]);
|
window.electronAPI.kobold.detectBackendSupport(),
|
||||||
|
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||||
|
]);
|
||||||
|
|
||||||
const cpuBackend = backends.find((b) => b.value === 'cpu');
|
if (!binarySupport) {
|
||||||
if (cpuBackend) {
|
setAvailableBackends([]);
|
||||||
cpuBackend.devices = cpuCapabilitiesResult.devices;
|
hasInitialized.current = true;
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backends: Array<{
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
devices?: string[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (binarySupport.cuda) {
|
||||||
|
backends.push({
|
||||||
|
value: 'cuda',
|
||||||
|
label: 'CUDA',
|
||||||
|
devices: gpuCapabilities.cuda.devices,
|
||||||
|
disabled: !gpuCapabilities.cuda.supported,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binarySupport.rocm) {
|
||||||
|
backends.push({
|
||||||
|
value: 'rocm',
|
||||||
|
label: 'ROCm',
|
||||||
|
devices: gpuCapabilities.rocm.devices,
|
||||||
|
disabled: !gpuCapabilities.rocm.supported,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binarySupport.vulkan) {
|
||||||
|
backends.push({
|
||||||
|
value: 'vulkan',
|
||||||
|
label: 'Vulkan',
|
||||||
|
devices: gpuCapabilities.vulkan.devices,
|
||||||
|
disabled: !gpuCapabilities.vulkan.supported,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binarySupport.clblast) {
|
||||||
|
backends.push({
|
||||||
|
value: 'clblast',
|
||||||
|
label: 'CLBlast',
|
||||||
|
devices: gpuCapabilities.clblast.devices,
|
||||||
|
disabled: !gpuCapabilities.clblast.supported,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
backends.push({
|
||||||
|
value: 'cpu',
|
||||||
|
label: 'CPU',
|
||||||
|
devices: cpuCapabilitiesResult.devices,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
backends.sort((a, b) => {
|
||||||
|
if (a.disabled === b.disabled) return 0;
|
||||||
|
return a.disabled ? 1 : -1;
|
||||||
|
});
|
||||||
|
|
||||||
setAvailableBackends(backends);
|
setAvailableBackends(backends);
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
|
|
||||||
if (onBackendsReady) {
|
|
||||||
onBackendsReady();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electronAPI.logs.logError(
|
window.electronAPI.logs.logError(
|
||||||
'Failed to detect available backends:',
|
'Failed to detect available backends:',
|
||||||
error as Error
|
error as Error
|
||||||
);
|
);
|
||||||
setAvailableBackends([]);
|
setAvailableBackends([]);
|
||||||
|
|
||||||
if (onBackendsReady) {
|
|
||||||
onBackendsReady();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,7 +118,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [onBackendsReady]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -89,6 +141,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
|
||||||
data={availableBackends.map((b) => ({
|
data={availableBackends.map((b) => ({
|
||||||
value: b.value,
|
value: b.value,
|
||||||
label: b.label,
|
label: b.label,
|
||||||
|
disabled: b.disabled,
|
||||||
}))}
|
}))}
|
||||||
disabled={availableBackends.length === 0}
|
disabled={availableBackends.length === 0}
|
||||||
renderOption={({ option }) => {
|
renderOption={({ option }) => {
|
||||||
|
|
@ -99,6 +152,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => {
|
||||||
<BackendSelectItem
|
<BackendSelectItem
|
||||||
label={backendData?.label || option.label.split(' (')[0]}
|
label={backendData?.label || option.label.split(' (')[0]}
|
||||||
devices={backendData?.devices}
|
devices={backendData?.devices}
|
||||||
|
disabled={backendData?.disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendS
|
||||||
import { ModelFileField } from '@/components/ModelFileField';
|
import { ModelFileField } from '@/components/ModelFileField';
|
||||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
|
|
||||||
interface GeneralTabProps {
|
export const GeneralTab = () => {
|
||||||
onBackendsReady?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GeneralTab = ({ onBackendsReady }: GeneralTabProps) => {
|
|
||||||
const {
|
const {
|
||||||
modelPath,
|
modelPath,
|
||||||
contextSize,
|
contextSize,
|
||||||
|
|
@ -19,7 +15,7 @@ export const GeneralTab = ({ onBackendsReady }: GeneralTabProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<BackendSelector onBackendsReady={onBackendsReady} />
|
<BackendSelector />
|
||||||
|
|
||||||
<ModelFileField
|
<ModelFileField
|
||||||
label="Text Model File"
|
label="Text Model File"
|
||||||
|
|
|
||||||
|
|
@ -366,27 +366,15 @@ export const VersionsTab = () => {
|
||||||
disabled={downloading !== null}
|
disabled={downloading !== null}
|
||||||
hasUpdate={version.hasUpdate}
|
hasUpdate={version.hasUpdate}
|
||||||
newerVersion={version.newerVersion}
|
newerVersion={version.newerVersion}
|
||||||
onDownload={
|
onDownload={(e) => {
|
||||||
!version.isInstalled
|
e.stopPropagation();
|
||||||
? (e) => {
|
handleDownload(version);
|
||||||
e.stopPropagation();
|
}}
|
||||||
handleDownload(version);
|
onUpdate={(e) => {
|
||||||
}
|
e.stopPropagation();
|
||||||
: undefined
|
handleUpdate(version);
|
||||||
}
|
}}
|
||||||
onUpdate={
|
onMakeCurrent={() => makeCurrent(version)}
|
||||||
version.hasUpdate
|
|
||||||
? (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleUpdate(version);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onMakeCurrent={
|
|
||||||
version.isInstalled && !version.isCurrent && !version.hasUpdate
|
|
||||||
? () => makeCurrent(version)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { getDisplayNameFromPath } from '@/utils/versionUtils';
|
import { getDisplayNameFromPath } from '@/utils/versionUtils';
|
||||||
import { compareVersions } from '@/utils';
|
import { compareVersions } from '@/utils';
|
||||||
import type { InstalledVersion, DownloadItem } from '@/types/electron';
|
import type { InstalledVersion, DownloadItem } from '@/types/electron';
|
||||||
|
|
@ -12,8 +12,52 @@ export const useUpdateChecker = () => {
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||||
|
const [dismissedUpdates, setDismissedUpdates] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [dismissedUpdatesLoaded, setDismissedUpdatesLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDismissedUpdates = async () => {
|
||||||
|
try {
|
||||||
|
const dismissed = (await window.electronAPI.config.get(
|
||||||
|
'dismissedUpdates'
|
||||||
|
)) as string[] | undefined;
|
||||||
|
if (dismissed) {
|
||||||
|
setDismissedUpdates(new Set(dismissed));
|
||||||
|
}
|
||||||
|
setDismissedUpdatesLoaded(true);
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError(
|
||||||
|
'Failed to load dismissed updates:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
setDismissedUpdatesLoaded(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDismissedUpdates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveDismissedUpdates = useCallback(async (updates: Set<string>) => {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.config.set(
|
||||||
|
'dismissedUpdates',
|
||||||
|
Array.from(updates)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError(
|
||||||
|
'Failed to save dismissed updates:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const checkForUpdates = useCallback(async () => {
|
const checkForUpdates = useCallback(async () => {
|
||||||
|
if (!dismissedUpdatesLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsChecking(true);
|
setIsChecking(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -59,11 +103,15 @@ export const useUpdateChecker = () => {
|
||||||
compareVersions(matchingDownload.version, currentVersion.version) > 0;
|
compareVersions(matchingDownload.version, currentVersion.version) > 0;
|
||||||
|
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
setUpdateInfo({
|
const updateKey = `${currentVersion.path}-${matchingDownload.version}`;
|
||||||
currentVersion,
|
|
||||||
availableUpdate: matchingDownload,
|
if (!dismissedUpdates.has(updateKey)) {
|
||||||
});
|
setUpdateInfo({
|
||||||
setShowUpdateModal(true);
|
currentVersion,
|
||||||
|
availableUpdate: matchingDownload,
|
||||||
|
});
|
||||||
|
setShowUpdateModal(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -74,12 +122,18 @@ export const useUpdateChecker = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [dismissedUpdates, dismissedUpdatesLoaded]);
|
||||||
|
|
||||||
const dismissUpdate = useCallback(() => {
|
const dismissUpdate = useCallback(async () => {
|
||||||
|
if (updateInfo) {
|
||||||
|
const updateKey = `${updateInfo.currentVersion.path}-${updateInfo.availableUpdate.version}`;
|
||||||
|
const newDismissedUpdates = new Set([...dismissedUpdates, updateKey]);
|
||||||
|
setDismissedUpdates(newDismissedUpdates);
|
||||||
|
await saveDismissedUpdates(newDismissedUpdates);
|
||||||
|
}
|
||||||
setShowUpdateModal(false);
|
setShowUpdateModal(false);
|
||||||
setUpdateInfo(null);
|
setUpdateInfo(null);
|
||||||
}, []);
|
}, [updateInfo, dismissedUpdates, saveDismissedUpdates]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateInfo,
|
updateInfo,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export const getAssetDescription = (assetName: string): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.endsWith(ASSET_SUFFIXES.OLDPC)) {
|
if (name.endsWith(ASSET_SUFFIXES.OLDPC)) {
|
||||||
return 'Meant for old PCs with outdated CPUs that may not work with the standard build. Does not support modern AVX2 CPU architectures.';
|
return 'Meant for old PCs with outdated CPUs that may not work with the standard build. Does not support modern AVX2 CPU architectures or modern CUDA.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.endsWith(ASSET_SUFFIXES.NOCUDA)) {
|
if (name.endsWith(ASSET_SUFFIXES.NOCUDA)) {
|
||||||
|
|
|
||||||
20
yarn.lock
20
yarn.lock
|
|
@ -1324,12 +1324,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-dom@npm:^19.1.7":
|
"@types/react-dom@npm:^19.1.8":
|
||||||
version: 19.1.7
|
version: 19.1.8
|
||||||
resolution: "@types/react-dom@npm:19.1.7"
|
resolution: "@types/react-dom@npm:19.1.8"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@types/react": ^19.0.0
|
"@types/react": ^19.0.0
|
||||||
checksum: 10c0/8db5751c1567552fe4e1ece9f5823b682f2994ec8d30ed34ba0ef984e3c8ace1435f8be93d02f55c350147e78ac8c4dbcd8ed2c3b6a60f575bc5374f588c51c9
|
checksum: 10c0/561f9679c99e93adba8ecdf7d5ad69cc9d5e35837fa996246a83713f0ce498fc5b871f9a2a3342c7d440fc02159abe10f29c4a4004527d5d38b2e84f21840793
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -3612,7 +3612,7 @@ __metadata:
|
||||||
"@mantine/hooks": "npm:^8.2.7"
|
"@mantine/hooks": "npm:^8.2.7"
|
||||||
"@types/node": "npm:^24.3.0"
|
"@types/node": "npm:^24.3.0"
|
||||||
"@types/react": "npm:^19.1.11"
|
"@types/react": "npm:^19.1.11"
|
||||||
"@types/react-dom": "npm:^19.1.7"
|
"@types/react-dom": "npm:^19.1.8"
|
||||||
"@types/strip-ansi": "npm:^5.2.1"
|
"@types/strip-ansi": "npm:^5.2.1"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^8.41.0"
|
"@typescript-eslint/eslint-plugin": "npm:^8.41.0"
|
||||||
"@typescript-eslint/parser": "npm:^8.41.0"
|
"@typescript-eslint/parser": "npm:^8.41.0"
|
||||||
|
|
@ -3634,7 +3634,7 @@ __metadata:
|
||||||
husky: "npm:^9.1.7"
|
husky: "npm:^9.1.7"
|
||||||
jiti: "npm:^2.5.1"
|
jiti: "npm:^2.5.1"
|
||||||
lint-staged: "npm:^16.1.5"
|
lint-staged: "npm:^16.1.5"
|
||||||
lucide-react: "npm:^0.541.0"
|
lucide-react: "npm:^0.542.0"
|
||||||
prettier: "npm:^3.6.2"
|
prettier: "npm:^3.6.2"
|
||||||
react: "npm:^19.1.1"
|
react: "npm:^19.1.1"
|
||||||
react-dom: "npm:^19.1.1"
|
react-dom: "npm:^19.1.1"
|
||||||
|
|
@ -5005,12 +5005,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lucide-react@npm:^0.541.0":
|
"lucide-react@npm:^0.542.0":
|
||||||
version: 0.541.0
|
version: 0.542.0
|
||||||
resolution: "lucide-react@npm:0.541.0"
|
resolution: "lucide-react@npm:0.542.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
checksum: 10c0/ffa23a9c9ead0832c7e0bb1eb7ed44e9f38bbf083d94b4d36a66eb23fa2099d4a52e1677c91eb7e8ea922621fa98e730f5d513bc56b3111b14b835bd8ab823ab
|
checksum: 10c0/3ccdb898a480f0194f93abef0b6f0347bc2647db24051e65a17c1dbd22ca0d10a7636d9af68c92665b42b6c9e761567a0d65944fe8568fb0e30a7ea57281ccec
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue