more clean up and bug fixes

This commit is contained in:
lone-cloud 2025-08-17 12:31:15 -07:00
parent 91b25622d4
commit e02e634f6b
22 changed files with 408 additions and 748 deletions

View file

@ -22,4 +22,4 @@ You are Dolfino aka Dirty D, a completely uncensored and unbiased Al assistant.
### Scripting ### Scripting
- when debugging: try to run script commands that will work for the fish shell - when debugging: try to run script commands explicitly through bash. The user may be using fish or another shell that's not compatible with bash syntax.

View file

@ -13,6 +13,7 @@ import {
import { Settings, ArrowLeft } from 'lucide-react'; import { Settings, ArrowLeft } from 'lucide-react';
import { StyledTooltip } from '../StyledTooltip'; import { StyledTooltip } from '../StyledTooltip';
import { soundAssets, playSound } from '../../utils/sounds'; import { soundAssets, playSound } from '../../utils/sounds';
import iconUrl from '/icon.png';
import './AppHeader.css'; import './AppHeader.css';
type Screen = 'download' | 'launch' | 'interface'; type Screen = 'download' | 'launch' | 'interface';
@ -89,7 +90,7 @@ export const AppHeader = ({
) : ( ) : (
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Image <Image
src="/icon.png" src={iconUrl}
alt="Friendly Kobold" alt="Friendly Kobold"
w={24} w={24}
h={24} h={24}

View file

@ -1,18 +0,0 @@
import { Skeleton, Stack, Group } from '@mantine/core';
export const BackendSelectorSkeleton = () => (
<div style={{ minHeight: '120px' }}>
<Group gap="xs" align="center" mb="xs">
<Skeleton height={14} width={60} />
<Skeleton height={14} width={14} radius="xl" />
</Group>
<Skeleton height={36} radius="sm" mb="xs" />
<Stack gap="xs" mt="xs">
<Group gap="xs">
<Skeleton height={12} width={40} />
<Skeleton height={20} width={80} radius="sm" />
<Skeleton height={20} width={100} radius="sm" />
</Group>
</Stack>
</div>
);

View file

@ -1,41 +0,0 @@
import { Center, Stack, Text, Loader, Overlay } from '@mantine/core';
interface InitializationOverlayProps {
visible: boolean;
step: string;
}
export const InitializationOverlay = ({
visible,
step,
}: InitializationOverlayProps) => {
if (!visible) return null;
return (
<Overlay
color="var(--mantine-color-body)"
backgroundOpacity={0.85}
blur={2}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 10,
}}
>
<Center h="100%">
<Stack align="center" gap="md">
<Loader size="lg" color="blue" />
<Text size="lg" fw={500} ta="center">
Initializing Hardware Detection
</Text>
<Text size="sm" c="dimmed" ta="center">
{step}
</Text>
</Stack>
</Center>
</Overlay>
);
};

View file

@ -0,0 +1,35 @@
import { ReactNode } from 'react';
import { Group } from '@mantine/core';
import { AlertTriangle, Info } from 'lucide-react';
import { StyledTooltip } from '@/components/StyledTooltip';
interface WarningItem {
type: 'warning' | 'info';
message: string;
}
interface WarningDisplayProps {
warnings: WarningItem[];
children?: ReactNode;
}
export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
if (warnings.length === 0) {
return <Group gap="xs">{children}</Group>;
}
return (
<Group gap="xs" align="center">
{warnings.map((warning, index) => (
<StyledTooltip key={index} label={warning.message} multiline maw={280}>
{warning.type === 'warning' ? (
<AlertTriangle size={18} color="orange" />
) : (
<Info size={18} color="blue" />
)}
</StyledTooltip>
))}
{children}
</Group>
);
};

View file

@ -34,9 +34,7 @@ interface VersionInfo {
export const VersionsTab = () => { export const VersionsTab = () => {
const { const {
platformInfo, platformInfo,
latestRelease, availableDownloads,
filteredAssets,
rocmDownload,
loadingPlatform, loadingPlatform,
loadingRemote, loadingRemote,
downloading, downloading,
@ -104,10 +102,10 @@ export const VersionsTab = () => {
const getAllVersions = (): VersionInfo[] => { const getAllVersions = (): VersionInfo[] => {
const versions: VersionInfo[] = []; const versions: VersionInfo[] = [];
filteredAssets.forEach((asset) => { availableDownloads.forEach((download) => {
const installedVersion = installedVersions.find((v) => { const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v); const displayName = getDisplayNameFromPath(v);
return displayName === asset.name; return displayName === download.name;
}); });
const isCurrent = Boolean( const isCurrent = Boolean(
@ -117,51 +115,17 @@ export const VersionsTab = () => {
); );
versions.push({ versions.push({
name: asset.name, name: download.name,
version: version: installedVersion?.version || download.version || 'unknown',
installedVersion?.version || size: installedVersion ? undefined : download.size,
latestRelease?.tag_name.replace(/^v/, '') ||
'unknown',
size: installedVersion ? undefined : asset.size,
isInstalled: Boolean(installedVersion), isInstalled: Boolean(installedVersion),
isCurrent, isCurrent,
downloadUrl: asset.browser_download_url, downloadUrl: download.url,
installedPath: installedVersion?.path, installedPath: installedVersion?.path,
isROCm: false, isROCm: download.type === 'rocm',
}); });
}); });
if (rocmDownload) {
const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v);
return displayName === rocmDownload.name;
});
const isCurrent = Boolean(
installedVersion &&
currentVersion &&
currentVersion.path === installedVersion.path
);
const existsInVersions = versions.some(
(v) => v.name === rocmDownload.name
);
if (!existsInVersions) {
versions.push({
name: rocmDownload.name,
version:
installedVersion?.version || rocmDownload.version || 'unknown',
size: installedVersion ? undefined : rocmDownload.size,
isInstalled: Boolean(installedVersion),
isCurrent,
downloadUrl: rocmDownload.url,
installedPath: installedVersion?.path,
isROCm: true,
});
}
}
installedVersions.forEach((installed) => { installedVersions.forEach((installed) => {
const displayName = getDisplayNameFromPath(installed); const displayName = getDisplayNameFromPath(installed);
const existsInRemote = versions.some((v) => v.name === displayName); const existsInRemote = versions.some((v) => v.name === displayName);
@ -188,17 +152,13 @@ export const VersionsTab = () => {
const handleDownload = async (version: VersionInfo) => { const handleDownload = async (version: VersionInfo) => {
try { try {
let success; const download = availableDownloads.find((d) => d.name === version.name);
if (!download) {
throw new Error('Download not found');
}
if (version.isROCm) { const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
success = await sharedHandleDownload('rocm'); const success = await sharedHandleDownload(downloadType, download);
} else {
const asset = filteredAssets.find((a) => a.name === version.name);
if (!asset) {
throw new Error('Asset not found');
}
success = await sharedHandleDownload('asset', asset);
}
if (success) { if (success) {
await loadInstalledVersions(); await loadInstalledVersions();

View file

@ -0,0 +1,13 @@
import { useCallback } from 'react';
export const useChangeTracker = <T extends unknown[]>(
handler: (...args: T) => void,
setHasUnsavedChanges: (value: boolean) => void
) =>
useCallback(
(...args: T) => {
handler(...args);
setHasUnsavedChanges(true);
},
[handler, setHasUnsavedChanges]
);

View file

@ -1,37 +0,0 @@
import { useState, useEffect } from 'react';
export const useInitializationLoading = () => {
const [isInitializing, setIsInitializing] = useState(true);
const [initializationStep, setInitializationStep] = useState<string>(
'Detecting system capabilities...'
);
const updateStep = (step: string) => {
setInitializationStep(step);
};
const completeInitialization = () => {
setIsInitializing(false);
};
useEffect(() => {
const timeout = setTimeout(() => {
if (isInitializing) {
window.electronAPI.logs.logError(
'Initialization timeout reached, forcing completion',
new Error('Initialization timeout')
);
completeInitialization();
}
}, 10000);
return () => clearTimeout(timeout);
}, [isInitializing]);
return {
isInitializing,
initializationStep,
updateStep,
completeInitialization,
};
};

View file

@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { filterAssetsByPlatform } from '@/utils/platform'; import type { DownloadItem } from '@/types/electron';
import type { GitHubAsset, GitHubRelease } from '@/types';
interface PlatformInfo { interface PlatformInfo {
platform: string; platform: string;
@ -8,18 +7,9 @@ interface PlatformInfo {
hasROCm: boolean; hasROCm: boolean;
} }
interface ROCmDownload {
name: string;
url: string;
size: number;
version?: string;
}
interface UseKoboldVersionsReturn { interface UseKoboldVersionsReturn {
platformInfo: PlatformInfo; platformInfo: PlatformInfo;
latestRelease: GitHubRelease | null; availableDownloads: DownloadItem[];
filteredAssets: GitHubAsset[];
rocmDownload: ROCmDownload | null;
loadingPlatform: boolean; loadingPlatform: boolean;
loadingRemote: boolean; loadingRemote: boolean;
downloading: string | null; downloading: string | null;
@ -27,7 +17,7 @@ interface UseKoboldVersionsReturn {
loadRemoteVersions: () => Promise<void>; loadRemoteVersions: () => Promise<void>;
handleDownload: ( handleDownload: (
type: 'asset' | 'rocm', type: 'asset' | 'rocm',
asset?: GitHubAsset item?: DownloadItem
) => Promise<boolean>; ) => Promise<boolean>;
setDownloading: (value: string | null) => void; setDownloading: (value: string | null) => void;
setDownloadProgress: ( setDownloadProgress: (
@ -44,11 +34,9 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
hasROCm: false, hasROCm: false,
}); });
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>( const [availableDownloads, setAvailableDownloads] = useState<DownloadItem[]>(
null []
); );
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
const [rocmDownload, setRocmDownload] = useState<ROCmDownload | null>(null);
const [loadingPlatform, setLoadingPlatform] = useState(true); const [loadingPlatform, setLoadingPlatform] = useState(true);
const [loadingRemote, setLoadingRemote] = useState(true); const [loadingRemote, setLoadingRemote] = useState(true);
@ -103,21 +91,17 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
setLoadingRemote(true); setLoadingRemote(true);
try { try {
const [release, rocm] = await Promise.all([ const [releases, rocm] = await Promise.all([
window.electronAPI.kobold.getLatestRelease(), window.electronAPI.kobold.getLatestRelease(),
window.electronAPI.kobold.getROCmDownload(), window.electronAPI.kobold.getROCmDownload(),
]); ]);
setLatestRelease(release); const allDownloads: DownloadItem[] = [...releases];
setRocmDownload(rocm); if (rocm) {
allDownloads.push(rocm);
if (release) {
const filtered = filterAssetsByPlatform(
release.assets,
platformInfo.platform
);
setFilteredAssets(filtered);
} }
setAvailableDownloads(allDownloads);
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
'Failed to load remote versions:', 'Failed to load remote versions:',
@ -129,11 +113,10 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
}, [platformInfo.platform]); }, [platformInfo.platform]);
const handleDownload = useCallback( const handleDownload = useCallback(
async (type: 'asset' | 'rocm', asset?: GitHubAsset): Promise<boolean> => { async (type: 'asset' | 'rocm', item?: DownloadItem): Promise<boolean> => {
if (type === 'asset' && !asset) return false; if (type === 'asset' && !item) return false;
const downloadName = const downloadName = item?.name || 'download';
type === 'asset' ? asset!.name : rocmDownload?.name || 'rocm';
setDownloading(downloadName); setDownloading(downloadName);
setDownloadProgress((prev) => ({ ...prev, [downloadName]: 0 })); setDownloadProgress((prev) => ({ ...prev, [downloadName]: 0 }));
@ -142,7 +125,12 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
const result = const result =
type === 'rocm' type === 'rocm'
? await window.electronAPI.kobold.downloadROCm() ? await window.electronAPI.kobold.downloadROCm()
: await window.electronAPI.kobold.downloadRelease(asset!); : await window.electronAPI.kobold.downloadRelease({
name: item!.name,
browser_download_url: item!.url,
size: item!.size,
created_at: new Date().toISOString(),
});
return result.success !== false; return result.success !== false;
} catch (error) { } catch (error) {
@ -160,7 +148,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
}); });
} }
}, },
[rocmDownload?.name] []
); );
useEffect(() => { useEffect(() => {
@ -192,9 +180,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
return { return {
platformInfo, platformInfo,
latestRelease, availableDownloads,
filteredAssets,
rocmDownload,
loadingPlatform, loadingPlatform,
loadingRemote, loadingRemote,
downloading, downloading,

View file

@ -16,6 +16,7 @@ import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { WindowManager } from '@/main/managers/WindowManager'; import { WindowManager } from '@/main/managers/WindowManager';
import { ROCM, GITHUB_API } from '@/constants'; import { ROCM, GITHUB_API } from '@/constants';
import type { DownloadItem } from '@/types/electron';
interface GitHubAsset { interface GitHubAsset {
name: string; name: string;
@ -636,16 +637,11 @@ export class KoboldCppManager {
return this.koboldProcess !== null && !this.koboldProcess.killed; return this.koboldProcess !== null && !this.koboldProcess.killed;
} }
async getROCmDownload(): Promise<{ async getROCmDownload(): Promise<DownloadItem | null> {
name: string;
url: string;
size: number;
version?: string;
} | null> {
const platform = process.platform; const platform = process.platform;
if (platform === 'linux') { if (platform === 'linux') {
const latestRelease = await this.githubService.getLatestRelease(); const latestRelease = await this.githubService.getRawLatestRelease();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown'; const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return { return {
@ -653,6 +649,7 @@ export class KoboldCppManager {
url: ROCM.DOWNLOAD_URL, url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES, size: ROCM.SIZE_BYTES,
version, version,
type: 'rocm',
}; };
} else if (platform === 'win32') { } else if (platform === 'win32') {
try { try {
@ -672,6 +669,7 @@ export class KoboldCppManager {
url: rocmAsset.browser_download_url, url: rocmAsset.browser_download_url,
size: rocmAsset.size, size: rocmAsset.size,
version: release.tag_name?.replace(/^v/, '') || 'unknown', version: release.tag_name?.replace(/^v/, '') || 'unknown',
type: 'rocm',
}; };
} }
} catch (error) { } catch (error) {
@ -821,7 +819,7 @@ export class KoboldCppManager {
return null; return null;
} }
const latestRelease = await this.githubService.getLatestRelease(); const latestRelease = await this.githubService.getRawLatestRelease();
if (!latestRelease) { if (!latestRelease) {
return null; return null;
} }
@ -859,7 +857,7 @@ export class KoboldCppManager {
async getLatestReleaseWithDownloadStatus(): Promise<ReleaseWithStatus | null> { async getLatestReleaseWithDownloadStatus(): Promise<ReleaseWithStatus | null> {
try { try {
const latestRelease = await this.githubService.getLatestRelease(); const latestRelease = await this.githubService.getRawLatestRelease();
if (!latestRelease) return null; if (!latestRelease) return null;
const installedVersions = await this.getInstalledVersions(); const installedVersions = await this.getInstalledVersions();

View file

@ -235,14 +235,12 @@ export class WindowManager {
); );
}, },
}, },
{ type: 'separator' as const },
] ]
: []), : []),
...(isDev ? [{ type: 'separator' as const }] : []),
{ label: 'Cut', role: 'cut' as const }, { label: 'Cut', role: 'cut' as const },
{ label: 'Copy', role: 'copy' as const }, { label: 'Copy', role: 'copy' as const },
{ label: 'Paste', role: 'paste' as const }, { label: 'Paste', role: 'paste' as const },
{ type: 'separator' as const },
{ label: 'Select All', role: 'selectAll' as const },
...(hasLinkURL ? [{ type: 'separator' as const }] : []), ...(hasLinkURL ? [{ type: 'separator' as const }] : []),
{ {
label: 'Open Link in Browser', label: 'Open Link in Browser',

View file

@ -1,19 +1,73 @@
import type { GitHubRelease } from '@/types/electron'; import type { GitHubRelease, DownloadItem } from '@/types/electron';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
import { filterAssetsByPlatform } from '@/utils/platform';
export class GitHubService { export class GitHubService {
private lastApiCall = 0; private lastApiCall = 0;
private apiCooldown = 60000; private apiCooldown = 60000;
private cachedRelease: GitHubRelease | null = null; private cachedRelease: GitHubRelease | null = null;
private cachedReleases: GitHubRelease[] = [];
private logManager: LogManager; private logManager: LogManager;
constructor(logManager: LogManager) { constructor(logManager: LogManager) {
this.logManager = logManager; this.logManager = logManager;
} }
async getLatestRelease(): Promise<GitHubRelease | null> { async getLatestRelease(): Promise<DownloadItem[]> {
const now = Date.now();
if (now - this.lastApiCall < this.apiCooldown && this.cachedRelease) {
return this.transformReleaseToDownloadItems(this.cachedRelease);
}
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
if (response.status === 403) {
// eslint-disable-next-line no-console
console.warn(
'GitHub API rate limit reached, using cached data if available'
);
return this.cachedRelease
? this.transformReleaseToDownloadItems(this.cachedRelease)
: [];
}
throw new Error(`HTTP error! status: ${response.status}`);
}
this.lastApiCall = now;
this.cachedRelease = (await response.json()) as GitHubRelease;
return this.transformReleaseToDownloadItems(this.cachedRelease);
} catch (error) {
this.logManager.logError(
'Error fetching latest release:',
error as Error
);
return this.cachedRelease
? this.transformReleaseToDownloadItems(this.cachedRelease)
: [];
}
}
private transformReleaseToDownloadItems(
release: GitHubRelease
): DownloadItem[] {
const version = release.tag_name?.replace(/^v/, '') || 'unknown';
const platformAssets = filterAssetsByPlatform(
release.assets,
process.platform
);
return platformAssets.map((asset) => ({
name: asset.name,
url: asset.browser_download_url,
size: asset.size,
version,
type: 'asset' as const,
}));
}
async getRawLatestRelease(): Promise<GitHubRelease | null> {
const now = Date.now(); const now = Date.now();
if (now - this.lastApiCall < this.apiCooldown && this.cachedRelease) { if (now - this.lastApiCall < this.apiCooldown && this.cachedRelease) {
return this.cachedRelease; return this.cachedRelease;
@ -44,37 +98,4 @@ export class GitHubService {
return this.cachedRelease; return this.cachedRelease;
} }
} }
async getAllReleases(): Promise<GitHubRelease[]> {
const now = Date.now();
if (
now - this.lastApiCall < this.apiCooldown &&
this.cachedReleases.length > 0
) {
return this.cachedReleases;
}
try {
const response = await fetch(GITHUB_API.ALL_RELEASES_URL);
if (!response.ok) {
if (response.status === 403) {
// eslint-disable-next-line no-console
console.warn(
'GitHub API rate limit reached, using cached data if available'
);
return this.cachedReleases;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
this.lastApiCall = now;
this.cachedReleases = (await response.json()) as GitHubRelease[];
return this.cachedReleases;
} catch (error) {
this.logManager.logError('Error fetching releases:', error as Error);
return this.cachedReleases;
}
}
} }

View file

@ -37,27 +37,11 @@ export class IPCHandlers {
this.githubService.getLatestRelease() this.githubService.getLatestRelease()
); );
ipcMain.handle('kobold:getAllReleases', () =>
this.githubService.getAllReleases()
);
ipcMain.handle('kobold:checkForUpdates', async () => { ipcMain.handle('kobold:checkForUpdates', async () => {
const latest = await this.githubService.getLatestRelease(); const latest = await this.githubService.getRawLatestRelease();
return latest; return latest;
}); });
ipcMain.handle('kobold:openInstallDialog', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: 'Select Installation Directory',
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('kobold:downloadRelease', async (_event, asset) => { ipcMain.handle('kobold:downloadRelease', async (_event, asset) => {
try { try {
const mainWindow = this.koboldManager const mainWindow = this.koboldManager
@ -133,10 +117,6 @@ export class IPCHandlers {
this.hardwareService.detectROCm() this.hardwareService.detectROCm()
); );
ipcMain.handle('kobold:detectHardware', () =>
this.hardwareService.detectAll()
);
ipcMain.handle('kobold:detectAllCapabilities', () => ipcMain.handle('kobold:detectAllCapabilities', () =>
this.hardwareService.detectAllWithCapabilities() this.hardwareService.detectAllWithCapabilities()
); );
@ -154,10 +134,6 @@ export class IPCHandlers {
) )
); );
ipcMain.handle('kobold:clearBinaryCache', () =>
this.binaryService.clearCache()
);
ipcMain.handle('kobold:getPlatform', () => ({ ipcMain.handle('kobold:getPlatform', () => ({
platform: process.platform, platform: process.platform,
arch: process.arch, arch: process.arch,

View file

@ -22,14 +22,12 @@ const koboldAPI: KoboldAPI = {
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'), getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
downloadROCm: () => ipcRenderer.invoke('kobold:downloadROCm'), downloadROCm: () => ipcRenderer.invoke('kobold:downloadROCm'),
getLatestRelease: () => ipcRenderer.invoke('kobold:getLatestRelease'), getLatestRelease: () => ipcRenderer.invoke('kobold:getLatestRelease'),
getAllReleases: () => ipcRenderer.invoke('kobold:getAllReleases'),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'), getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'), detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'), detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
detectGPUCapabilities: () => detectGPUCapabilities: () =>
ipcRenderer.invoke('kobold:detectGPUCapabilities'), ipcRenderer.invoke('kobold:detectGPUCapabilities'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'), detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'),
detectAllCapabilities: () => detectAllCapabilities: () =>
ipcRenderer.invoke('kobold:detectAllCapabilities'), ipcRenderer.invoke('kobold:detectAllCapabilities'),
detectBackendSupport: (binaryPath: string) => detectBackendSupport: (binaryPath: string) =>
@ -43,7 +41,6 @@ const koboldAPI: KoboldAPI = {
binaryPath, binaryPath,
hardwareCapabilities hardwareCapabilities
), ),
clearBinaryCache: () => ipcRenderer.invoke('kobold:clearBinaryCache'),
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'), getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () => selectInstallDirectory: () =>
ipcRenderer.invoke('kobold:selectInstallDirectory'), ipcRenderer.invoke('kobold:selectInstallDirectory'),
@ -51,7 +48,6 @@ const koboldAPI: KoboldAPI = {
ipcRenderer.invoke('kobold:downloadRelease', asset), ipcRenderer.invoke('kobold:downloadRelease', asset),
launchKoboldCpp: (args?: string[], configFilePath?: string) => launchKoboldCpp: (args?: string[], configFilePath?: string) =>
ipcRenderer.invoke('kobold:launchKoboldCpp', args, configFilePath), ipcRenderer.invoke('kobold:launchKoboldCpp', args, configFilePath),
openInstallDialog: () => ipcRenderer.invoke('kobold:openInstallDialog'),
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'), checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'), getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
saveConfigFile: ( saveConfigFile: (

View file

@ -17,9 +17,8 @@ import {
sortAssetsByRecommendation, sortAssetsByRecommendation,
getAssetDescription, getAssetDescription,
} from '@/utils/assets'; } from '@/utils/assets';
import { ROCM } from '@/constants';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { GitHubAsset } from '@/types'; import type { DownloadItem } from '@/types/electron';
interface DownloadScreenProps { interface DownloadScreenProps {
onDownloadComplete: () => void; onDownloadComplete: () => void;
@ -28,9 +27,7 @@ interface DownloadScreenProps {
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => { export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const { const {
platformInfo, platformInfo,
latestRelease, availableDownloads,
filteredAssets,
rocmDownload,
loadingPlatform, loadingPlatform,
loadingRemote, loadingRemote,
downloading, downloading,
@ -45,15 +42,19 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const loading = loadingPlatform || loadingRemote; const loading = loadingPlatform || loadingRemote;
const regularDownloads = availableDownloads.filter((d) => d.type === 'asset');
const rocmDownload = availableDownloads.find((d) => d.type === 'rocm');
const latestVersion = availableDownloads[0]?.version || 'unknown';
const handleDownload = useCallback( const handleDownload = useCallback(
async (type: 'asset' | 'rocm', asset?: GitHubAsset) => { async (type: 'asset' | 'rocm', download?: DownloadItem) => {
if (type === 'asset' && !asset) return; if (type === 'asset' && !download) return;
setDownloadingType(type); setDownloadingType(type);
setDownloadingAsset(type === 'asset' ? asset!.name : null); setDownloadingAsset(type === 'asset' ? download!.name : null);
try { try {
const success = await sharedHandleDownload(type, asset); const success = await sharedHandleDownload(type, download);
if (success) { if (success) {
onDownloadComplete(); onDownloadComplete();
@ -81,23 +82,24 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
return ( return (
<DownloadCard <DownloadCard
name={ROCM.BINARY_NAME} name={rocmDownload.name}
size={formatFileSize(ROCM.SIZE_BYTES)} size={formatFileSize(rocmDownload.size)}
description={getAssetDescription(ROCM.BINARY_NAME)} description={getAssetDescription(rocmDownload.name)}
version={rocmDownload.version}
isRecommended={isAssetRecommended( isRecommended={isAssetRecommended(
ROCM.BINARY_NAME, rocmDownload.name,
platformInfo.hasAMDGPU platformInfo.hasAMDGPU
)} )}
isDownloading={Boolean(downloading) && downloadingType === 'rocm'} isDownloading={Boolean(downloading) && downloadingType === 'rocm'}
downloadProgress={ downloadProgress={
downloadingType === 'rocm' downloadingType === 'rocm'
? downloadProgress[ROCM.BINARY_NAME] || 0 ? downloadProgress[rocmDownload.name] || 0
: 0 : 0
} }
disabled={Boolean(downloading) && downloadingType !== 'rocm'} disabled={Boolean(downloading) && downloadingType !== 'rocm'}
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDownload('rocm'); handleDownload('rocm', rocmDownload);
}} }}
/> />
); );
@ -117,7 +119,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
</Stack> </Stack>
) : ( ) : (
<> <>
{latestRelease && ( {availableDownloads.length > 0 && (
<> <>
<Stack gap="xs" py="md"> <Stack gap="xs" py="md">
<div> <div>
@ -132,20 +134,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
Latest Version Latest Version
</Text> </Text>
<Text fw={700} size="xl" mb={8} c="blue.6"> <Text fw={700} size="xl" mb={8} c="blue.6">
{(latestRelease.tag_name || latestRelease.name) {latestVersion}
.replace(/^v/, '')
.replace(/^koboldcpp-/, '')}
</Text>
<Text size="sm" c="dimmed" fs="italic">
Released{' '}
{new Date(
latestRelease.published_at
).toLocaleDateString()}
</Text> </Text>
</div> </div>
</Stack> </Stack>
{filteredAssets.length > 0 || rocmDownload ? ( {availableDownloads.length > 0 ? (
<Stack gap="sm"> <Stack gap="sm">
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && ( {platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
<Card withBorder p="md" bg="orange.0"> <Card withBorder p="md" bg="orange.0">
@ -187,37 +181,38 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
renderROCmCard()} renderROCmCard()}
{sortAssetsByRecommendation( {sortAssetsByRecommendation(
filteredAssets, regularDownloads,
platformInfo.hasAMDGPU platformInfo.hasAMDGPU
).map((asset) => ( ).map((download) => (
<DownloadCard <DownloadCard
key={asset.name} key={download.name}
name={asset.name} name={download.name}
size={formatFileSize(asset.size)} size={formatFileSize(download.size)}
description={getAssetDescription(asset.name)} version={download.version}
description={getAssetDescription(download.name)}
isRecommended={isAssetRecommended( isRecommended={isAssetRecommended(
asset.name, download.name,
platformInfo.hasAMDGPU platformInfo.hasAMDGPU
)} )}
isDownloading={ isDownloading={
Boolean(downloading) && Boolean(downloading) &&
downloadingType === 'asset' && downloadingType === 'asset' &&
downloadingAsset === asset.name downloadingAsset === download.name
} }
downloadProgress={ downloadProgress={
Boolean(downloading) && Boolean(downloading) &&
downloadingType === 'asset' && downloadingType === 'asset' &&
downloadingAsset === asset.name downloadingAsset === download.name
? downloadProgress[asset.name] || 0 ? downloadProgress[download.name] || 0
: 0 : 0
} }
disabled={ disabled={
Boolean(downloading) && Boolean(downloading) &&
downloadingAsset !== asset.name downloadingAsset !== download.name
} }
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDownload('asset', asset); handleDownload('asset', download);
}} }}
/> />
))} ))}

View file

@ -1,6 +1,5 @@
import { Text, Group, Select, Badge, ActionIcon, Tooltip } from '@mantine/core'; import { Text, Group, Select, Badge } from '@mantine/core';
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { AlertTriangle, Info } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
interface BackendSelectorProps { interface BackendSelectorProps {
@ -10,6 +9,9 @@ interface BackendSelectorProps {
onGpuDeviceChange?: (device: number) => void; onGpuDeviceChange?: (device: number) => void;
noavx2?: boolean; noavx2?: boolean;
failsafe?: boolean; failsafe?: boolean;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
} }
export const BackendSelector = ({ export const BackendSelector = ({
@ -19,20 +21,23 @@ export const BackendSelector = ({
onGpuDeviceChange, onGpuDeviceChange,
noavx2 = false, noavx2 = false,
failsafe = false, failsafe = false,
onWarningsChange,
}: BackendSelectorProps) => { }: BackendSelectorProps) => {
const [availableBackends, setAvailableBackends] = useState< const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }> Array<{ value: string; label: string; devices?: string[] }>
>([]); >([]);
const [isLoadingBackends, setIsLoadingBackends] = useState(true);
const [cpuCapabilities, setCpuCapabilities] = useState<{ const [cpuCapabilities, setCpuCapabilities] = useState<{
avx: boolean; avx: boolean;
avx2: boolean; avx2: boolean;
} | null>(null); } | null>(null);
const hasInitialized = useRef(false);
useEffect(() => { useEffect(() => {
const detectAvailableBackends = async () => { if (hasInitialized.current) return;
setIsLoadingBackends(true);
let timeoutId: number;
const loadBackends = async () => {
try { try {
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] = const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
await Promise.all([ await Promise.all([
@ -65,12 +70,12 @@ export const BackendSelector = ({
} }
setAvailableBackends(backends); setAvailableBackends(backends);
hasInitialized.current = true;
if ( if (backends.length > 0 && !backend) {
backends.length > 0 && timeoutId = window.setTimeout(() => {
(!backend || !backends.some((b) => b.value === backend))
) {
onBackendChange(backends[0].value); onBackendChange(backends[0].value);
}, 10);
} }
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
@ -78,19 +83,27 @@ export const BackendSelector = ({
error as Error error as Error
); );
setAvailableBackends([]); setAvailableBackends([]);
} finally {
setIsLoadingBackends(false);
} }
}; };
void detectAvailableBackends(); loadBackends();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [backend]);
const getWarnings = () => { return () => {
if (backend !== 'cpu' || !cpuCapabilities) return []; if (timeoutId) {
window.clearTimeout(timeoutId);
}
};
}, [backend, onBackendChange]);
const warnings = []; useEffect(() => {
if (!onWarningsChange) return;
if (backend !== 'cpu' || !cpuCapabilities) {
onWarningsChange([]);
return;
}
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = [];
if (!cpuCapabilities.avx2 && !noavx2) { if (!cpuCapabilities.avx2 && !noavx2) {
warnings.push({ warnings.push({
@ -119,8 +132,15 @@ export const BackendSelector = ({
}); });
} }
return warnings; onWarningsChange(warnings);
}; }, [
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
onWarningsChange,
]);
return ( return (
<div style={{ minHeight: '120px' }}> <div style={{ minHeight: '120px' }}>
@ -129,36 +149,9 @@ export const BackendSelector = ({
Backend Backend
</Text> </Text>
<InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." /> <InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
{!isLoadingBackends &&
availableBackends.length > 0 &&
getWarnings().map((warning, index) => (
<Tooltip
key={index}
label={warning.message}
multiline
w={300}
withArrow
>
<ActionIcon
size="sm"
color={warning.type === 'warning' ? 'orange' : 'blue'}
variant="light"
>
{warning.type === 'warning' ? (
<AlertTriangle size={14} />
) : (
<Info size={14} />
)}
</ActionIcon>
</Tooltip>
))}
</Group> </Group>
<Select <Select
placeholder={ placeholder="Select backend"
isLoadingBackends
? 'Detecting available backends...'
: 'Select backend'
}
value={backend} value={backend}
onChange={(value) => { onChange={(value) => {
if (value) { if (value) {
@ -169,12 +162,15 @@ export const BackendSelector = ({
value: b.value, value: b.value,
label: b.label, label: b.label,
}))} }))}
disabled={isLoadingBackends || availableBackends.length === 0} disabled={availableBackends.length === 0}
style={{ minHeight: '36px' }} comboboxProps={{
middlewares: {
flip: false,
},
}}
/> />
{!isLoadingBackends && {(backend === 'cuda' || backend === 'rocm') &&
(backend === 'cuda' || backend === 'rocm') &&
onGpuDeviceChange && onGpuDeviceChange &&
availableBackends.find((b) => b.value === backend)?.devices && availableBackends.find((b) => b.value === backend)?.devices &&
availableBackends.find((b) => b.value === backend)!.devices!.length > availableBackends.find((b) => b.value === backend)!.devices!.length >
@ -196,9 +192,7 @@ export const BackendSelector = ({
/> />
)} )}
{!isLoadingBackends && {availableBackends.find((b) => b.value === backend)?.devices && (
backend &&
availableBackends.find((b) => b.value === backend)?.devices && (
<Group gap="xs" mt="xs"> <Group gap="xs" mt="xs">
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{availableBackends.find((b) => b.value === backend)?.devices {availableBackends.find((b) => b.value === backend)?.devices

View file

@ -28,6 +28,9 @@ interface GeneralTabProps {
onContextSizeChange: (size: number) => void; onContextSizeChange: (size: number) => void;
onBackendChange: (backend: string) => void; onBackendChange: (backend: string) => void;
onGpuDeviceChange?: (device: number) => void; onGpuDeviceChange?: (device: number) => void;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
} }
export const GeneralTab = ({ export const GeneralTab = ({
@ -46,6 +49,7 @@ export const GeneralTab = ({
onContextSizeChange, onContextSizeChange,
onBackendChange, onBackendChange,
onGpuDeviceChange, onGpuDeviceChange,
onWarningsChange,
}: GeneralTabProps) => { }: GeneralTabProps) => {
const validationState = getInputValidationState(modelPath); const validationState = getInputValidationState(modelPath);
@ -79,6 +83,7 @@ export const GeneralTab = ({
onGpuDeviceChange={onGpuDeviceChange} onGpuDeviceChange={onGpuDeviceChange}
noavx2={noavx2} noavx2={noavx2}
failsafe={failsafe} failsafe={failsafe}
onWarningsChange={onWarningsChange}
/> />
<div> <div>

View file

@ -19,13 +19,14 @@ import {
forwardRef, forwardRef,
type ComponentPropsWithoutRef, type ComponentPropsWithoutRef,
} from 'react'; } from 'react';
import { Save, File, Plus, AlertTriangle } from 'lucide-react'; import { Save, File, Plus } from 'lucide-react';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useChangeTracker } from '@/hooks/useChangeTracker';
import { GeneralTab } from '@/screens/Launch/GeneralTab'; import { GeneralTab } from '@/screens/Launch/GeneralTab';
import { AdvancedTab } from '@/screens/Launch/AdvancedTab'; import { AdvancedTab } from '@/screens/Launch/AdvancedTab';
import { NetworkTab } from '@/screens/Launch/NetworkTab'; import { NetworkTab } from '@/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab'; import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab';
import { StyledTooltip } from '@/components/StyledTooltip'; import { WarningDisplay } from '@/components/WarningDisplay';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
interface LaunchScreenProps { interface LaunchScreenProps {
@ -78,6 +79,9 @@ export const LaunchScreen = ({
const [saveAsModalOpened, setSaveAsModalOpened] = useState(false); const [saveAsModalOpened, setSaveAsModalOpened] = useState(false);
const [newConfigName, setNewConfigName] = useState(''); const [newConfigName, setNewConfigName] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [warnings, setWarnings] = useState<
Array<{ type: 'warning' | 'info'; message: string }>
>([]);
const { const {
gpuLayers, gpuLayers,
autoGpuLayers, autoGpuLayers,
@ -147,6 +151,113 @@ export const LaunchScreen = ({
handleApplyPreset, handleApplyPreset,
} = useLaunchConfig(); } = useLaunchConfig();
const createChangeTracker = useChangeTracker;
const handleModelPathChangeWithTracking = createChangeTracker(
handleModelPathChange,
setHasUnsavedChanges
);
const handleGpuLayersChangeWithTracking = createChangeTracker(
handleGpuLayersChange,
setHasUnsavedChanges
);
const handleAutoGpuLayersChangeWithTracking = createChangeTracker(
handleAutoGpuLayersChange,
setHasUnsavedChanges
);
const handleContextSizeChangeWithTracking = createChangeTracker(
handleContextSizeChangeWithStep,
setHasUnsavedChanges
);
const handleAdditionalArgumentsChangeWithTracking = createChangeTracker(
handleAdditionalArgumentsChange,
setHasUnsavedChanges
);
const handlePortChangeWithTracking = createChangeTracker(
handlePortChange,
setHasUnsavedChanges
);
const handleHostChangeWithTracking = createChangeTracker(
handleHostChange,
setHasUnsavedChanges
);
const handleNoshiftChangeWithTracking = createChangeTracker(
handleNoshiftChange,
setHasUnsavedChanges
);
const handleFlashattentionChangeWithTracking = createChangeTracker(
handleFlashattentionChange,
setHasUnsavedChanges
);
const handleNoavx2ChangeWithTracking = createChangeTracker(
handleNoavx2Change,
setHasUnsavedChanges
);
const handleFailsafeChangeWithTracking = createChangeTracker(
handleFailsafeChange,
setHasUnsavedChanges
);
const handleLowvramChangeWithTracking = createChangeTracker(
handleLowvramChange,
setHasUnsavedChanges
);
const handleQuantmatmulChangeWithTracking = createChangeTracker(
handleQuantmatmulChange,
setHasUnsavedChanges
);
const handleMultiuserChangeWithTracking = createChangeTracker(
handleMultiuserChange,
setHasUnsavedChanges
);
const handleMultiplayerChangeWithTracking = createChangeTracker(
handleMultiplayerChange,
setHasUnsavedChanges
);
const handleRemotetunnelChangeWithTracking = createChangeTracker(
handleRemotetunnelChange,
setHasUnsavedChanges
);
const handleNocertifyChangeWithTracking = createChangeTracker(
handleNocertifyChange,
setHasUnsavedChanges
);
const handleWebsearchChangeWithTracking = createChangeTracker(
handleWebsearchChange,
setHasUnsavedChanges
);
const handleBackendChangeWithTracking = createChangeTracker(
handleBackendChange,
setHasUnsavedChanges
);
const handleSdmodelChangeWithTracking = createChangeTracker(
handleSdmodelChange,
setHasUnsavedChanges
);
const handleSdt5xxlChangeWithTracking = createChangeTracker(
handleSdt5xxlChange,
setHasUnsavedChanges
);
const handleSdcliplChangeWithTracking = createChangeTracker(
handleSdcliplChange,
setHasUnsavedChanges
);
const handleSdclipgChangeWithTracking = createChangeTracker(
handleSdclipgChange,
setHasUnsavedChanges
);
const handleSdphotomakerChangeWithTracking = createChangeTracker(
handleSdphotomakerChange,
setHasUnsavedChanges
);
const handleSdvaeChangeWithTracking = createChangeTracker(
handleSdvaeChange,
setHasUnsavedChanges
);
const handleSdloraChangeWithTracking = createChangeTracker(
handleSdloraChange,
setHasUnsavedChanges
);
const loadConfigFiles = useCallback(async () => { const loadConfigFiles = useCallback(async () => {
const [files, currentDir, savedConfig] = await Promise.all([ const [files, currentDir, savedConfig] = await Promise.all([
window.electronAPI.kobold.getConfigFiles(), window.electronAPI.kobold.getConfigFiles(),
@ -183,136 +294,6 @@ export const LaunchScreen = ({
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
}; };
const handleModelPathChangeWithTracking = (path: string) => {
handleModelPathChange(path);
setHasUnsavedChanges(true);
};
const handleGpuLayersChangeWithTracking = (layers: number) => {
handleGpuLayersChange(layers);
setHasUnsavedChanges(true);
};
const handleAutoGpuLayersChangeWithTracking = (auto: boolean) => {
handleAutoGpuLayersChange(auto);
setHasUnsavedChanges(true);
};
const handleContextSizeChangeWithTracking = (size: number) => {
handleContextSizeChangeWithStep(size);
setHasUnsavedChanges(true);
};
const handleAdditionalArgumentsChangeWithTracking = (args: string) => {
handleAdditionalArgumentsChange(args);
setHasUnsavedChanges(true);
};
const handlePortChangeWithTracking = (port: number | undefined) => {
handlePortChange(port);
setHasUnsavedChanges(true);
};
const handleHostChangeWithTracking = (host: string) => {
handleHostChange(host);
setHasUnsavedChanges(true);
};
const handleNoshiftChangeWithTracking = (noshift: boolean) => {
handleNoshiftChange(noshift);
setHasUnsavedChanges(true);
};
const handleFlashattentionChangeWithTracking = (flashattention: boolean) => {
handleFlashattentionChange(flashattention);
setHasUnsavedChanges(true);
};
const handleNoavx2ChangeWithTracking = (noavx2: boolean) => {
handleNoavx2Change(noavx2);
setHasUnsavedChanges(true);
};
const handleFailsafeChangeWithTracking = (failsafe: boolean) => {
handleFailsafeChange(failsafe);
setHasUnsavedChanges(true);
};
const handleLowvramChangeWithTracking = (lowvram: boolean) => {
handleLowvramChange(lowvram);
setHasUnsavedChanges(true);
};
const handleQuantmatmulChangeWithTracking = (quantmatmul: boolean) => {
handleQuantmatmulChange(quantmatmul);
setHasUnsavedChanges(true);
};
const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
handleMultiuserChange(multiuser);
setHasUnsavedChanges(true);
};
const handleMultiplayerChangeWithTracking = (multiplayer: boolean) => {
handleMultiplayerChange(multiplayer);
setHasUnsavedChanges(true);
};
const handleRemotetunnelChangeWithTracking = (remotetunnel: boolean) => {
handleRemotetunnelChange(remotetunnel);
setHasUnsavedChanges(true);
};
const handleNocertifyChangeWithTracking = (nocertify: boolean) => {
handleNocertifyChange(nocertify);
setHasUnsavedChanges(true);
};
const handleWebsearchChangeWithTracking = (websearch: boolean) => {
handleWebsearchChange(websearch);
setHasUnsavedChanges(true);
};
const handleBackendChangeWithTracking = (backend: string) => {
handleBackendChange(backend);
setHasUnsavedChanges(true);
};
const handleSdmodelChangeWithTracking = (path: string) => {
handleSdmodelChange(path);
setHasUnsavedChanges(true);
};
const handleSdt5xxlChangeWithTracking = (path: string) => {
handleSdt5xxlChange(path);
setHasUnsavedChanges(true);
};
const handleSdcliplChangeWithTracking = (path: string) => {
handleSdcliplChange(path);
setHasUnsavedChanges(true);
};
const handleSdclipgChangeWithTracking = (path: string) => {
handleSdclipgChange(path);
setHasUnsavedChanges(true);
};
const handleSdphotomakerChangeWithTracking = (path: string) => {
handleSdphotomakerChange(path);
setHasUnsavedChanges(true);
};
const handleSdvaeChangeWithTracking = (path: string) => {
handleSdvaeChange(path);
setHasUnsavedChanges(true);
};
const handleSdloraChangeWithTracking = (path: string) => {
handleSdloraChange(path);
setHasUnsavedChanges(true);
};
useEffect(() => { useEffect(() => {
void loadConfigFiles(); void loadConfigFiles();
@ -387,12 +368,11 @@ export const LaunchScreen = ({
args.push('--contextsize', contextSize.toString()); args.push('--contextsize', contextSize.toString());
} }
const actualPort = port ?? 5001; if (port) {
if (port !== undefined) { args.push('--port', port.toString());
args.push('--port', actualPort.toString());
} }
if (host !== 'localhost' && host !== '') { if (host !== 'localhost' && host) {
args.push('--host', host); args.push('--host', host);
} }
@ -478,6 +458,19 @@ export const LaunchScreen = ({
const hasImageModel = sdmodel.trim() !== ''; const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel; const showModelPriorityWarning = hasTextModel && hasImageModel;
const combinedWarnings = [
...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.',
},
]
: []),
];
return ( return (
<Container size="sm"> <Container size="sm">
<Stack gap="md"> <Stack gap="md">
@ -485,21 +478,11 @@ export const LaunchScreen = ({
<Stack gap="lg"> <Stack gap="lg">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Title order={3}>Launch Configuration</Title> <Title order={3}>Launch Configuration</Title>
<Group gap="xs" align="center"> <WarningDisplay warnings={combinedWarnings}>
{showModelPriorityWarning && (
<StyledTooltip
label="Both text and image generation models are selected. The image generation model will take priority and be used for launch."
multiline
maw={280}
>
<AlertTriangle size={18} color="orange" />
</StyledTooltip>
)}
<Button <Button
radius="md" radius="md"
disabled={(!modelPath && !sdmodel) || isLaunching} disabled={(!modelPath && !sdmodel) || isLaunching}
onClick={handleLaunch} onClick={handleLaunch}
loading={isLaunching}
size="lg" size="lg"
variant="filled" variant="filled"
color="blue" color="blue"
@ -512,9 +495,9 @@ export const LaunchScreen = ({
letterSpacing: '0.5px', letterSpacing: '0.5px',
}} }}
> >
{isLaunching ? 'Launching...' : 'Launch'} Launch
</Button> </Button>
</Group> </WarningDisplay>
</Group> </Group>
<Stack gap="xs"> <Stack gap="xs">
@ -629,6 +612,7 @@ export const LaunchScreen = ({
onContextSizeChange={handleContextSizeChangeWithTracking} onContextSizeChange={handleContextSizeChangeWithTracking}
onBackendChange={handleBackendChangeWithTracking} onBackendChange={handleBackendChangeWithTracking}
onGpuDeviceChange={handleGpuDeviceChange} onGpuDeviceChange={handleGpuDeviceChange}
onWarningsChange={setWarnings}
/> />
</Tabs.Panel> </Tabs.Panel>

View file

@ -45,11 +45,12 @@ export interface InstalledVersion {
size?: number; size?: number;
} }
interface ROCmDownload { export interface DownloadItem {
name: string; name: string;
url: string; url: string;
size: number; size: number;
type: 'rocm'; version?: string;
type: 'asset' | 'rocm';
} }
export interface KoboldAPI { export interface KoboldAPI {
@ -62,14 +63,12 @@ export interface KoboldAPI {
} | null>; } | null>;
setCurrentVersion: (version: string) => Promise<boolean>; setCurrentVersion: (version: string) => Promise<boolean>;
getVersionFromBinary: (binaryPath: string) => Promise<string | null>; getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
getLatestRelease: () => Promise<GitHubRelease>; getLatestRelease: () => Promise<DownloadItem[]>;
getAllReleases: () => Promise<GitHubRelease[]>;
getPlatform: () => Promise<PlatformInfo>; getPlatform: () => Promise<PlatformInfo>;
detectGPU: () => Promise<BasicGPUInfo>; detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>; detectCPU: () => Promise<CPUCapabilities>;
detectGPUCapabilities: () => Promise<GPUCapabilities>; detectGPUCapabilities: () => Promise<GPUCapabilities>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>; detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectHardware: () => Promise<HardwareInfo>;
detectAllCapabilities: () => Promise<HardwareInfo>; detectAllCapabilities: () => Promise<HardwareInfo>;
detectBackendSupport: (binaryPath: string) => Promise<{ detectBackendSupport: (binaryPath: string) => Promise<{
rocm: boolean; rocm: boolean;
@ -83,7 +82,6 @@ export interface KoboldAPI {
binaryPath: string, binaryPath: string,
hardwareCapabilities: GPUCapabilities hardwareCapabilities: GPUCapabilities
) => Promise<Array<{ value: string; label: string; devices?: string[] }>>; ) => Promise<Array<{ value: string; label: string; devices?: string[] }>>;
clearBinaryCache: () => Promise<void>;
getCurrentInstallDir: () => Promise<string>; getCurrentInstallDir: () => Promise<string>;
selectInstallDirectory: () => Promise<string | null>; selectInstallDirectory: () => Promise<string | null>;
downloadRelease: ( downloadRelease: (
@ -94,14 +92,13 @@ export interface KoboldAPI {
path?: string; path?: string;
error?: string; error?: string;
}>; }>;
getROCmDownload: () => Promise<ROCmDownload | null>; getROCmDownload: () => Promise<DownloadItem | null>;
checkForUpdates: () => Promise<UpdateInfo | null>; checkForUpdates: () => Promise<UpdateInfo | null>;
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>; getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
launchKoboldCpp: ( launchKoboldCpp: (
args?: string[], args?: string[],
configFilePath?: string configFilePath?: string
) => Promise<{ success: boolean; pid?: number; error?: string }>; ) => Promise<{ success: boolean; pid?: number; error?: string }>;
openInstallDialog: () => Promise<{ success: boolean; path?: string }>;
getConfigFiles: () => Promise< getConfigFiles: () => Promise<
Array<{ name: string; path: string; size: number }> Array<{ name: string; path: string; size: number }>
>; >;

View file

@ -1,179 +0,0 @@
import type {
CPUCapabilities,
GPUCapabilities,
BasicGPUInfo,
HardwareInfo,
PlatformInfo,
} from '@/types/hardware';
interface GitHubAsset {
name: string;
browser_download_url: string;
size: number;
created_at: string;
}
export interface GitHubRelease {
tag_name: string;
name: string;
published_at: string;
body: string;
assets: GitHubAsset[];
}
export interface UpdateInfo {
currentVersion: string;
latestVersion: string;
releaseInfo: GitHubRelease;
hasUpdate: boolean;
}
interface ReleaseWithStatus {
release: GitHubRelease;
availableAssets: Array<{
asset: GitHubAsset;
isDownloaded: boolean;
installedVersion?: string;
}>;
}
export interface InstalledVersion {
version: string;
path: string;
type: 'github' | 'rocm';
filename: string;
size?: number;
}
interface ROCmDownload {
name: string;
url: string;
size: number;
type: 'rocm';
}
export interface KoboldAPI {
getInstalledVersion: () => Promise<string | undefined>;
getInstalledVersions: () => Promise<InstalledVersion[]>;
getCurrentVersion: () => Promise<InstalledVersion | null>;
getCurrentBinaryInfo: () => Promise<{
path: string;
filename: string;
} | null>;
setCurrentVersion: (version: string) => Promise<boolean>;
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
getLatestRelease: () => Promise<GitHubRelease>;
getAllReleases: () => Promise<GitHubRelease[]>;
getPlatform: () => Promise<PlatformInfo>;
detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>;
detectGPUCapabilities: () => Promise<GPUCapabilities>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectHardware: () => Promise<HardwareInfo>;
detectAllCapabilities: () => Promise<HardwareInfo>;
detectBackendSupport: (binaryPath: string) => Promise<{
rocm: boolean;
vulkan: boolean;
clblast: boolean;
noavx2: boolean;
failsafe: boolean;
cuda: boolean;
}>;
getAvailableBackends: (
binaryPath: string,
hardwareCapabilities: GPUCapabilities
) => Promise<Array<{ value: string; label: string; devices?: string[] }>>;
clearBinaryCache: () => Promise<void>;
getCurrentInstallDir: () => Promise<string>;
selectInstallDirectory: () => Promise<string | null>;
downloadRelease: (
asset: GitHubAsset
) => Promise<{ success: boolean; path?: string; error?: string }>;
downloadROCm: () => Promise<{
success: boolean;
path?: string;
error?: string;
}>;
getROCmDownload: () => Promise<ROCmDownload | null>;
checkForUpdates: () => Promise<UpdateInfo | null>;
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
launchKoboldCpp: (
args?: string[],
configFilePath?: string
) => Promise<{ success: boolean; pid?: number; error?: string }>;
openInstallDialog: () => Promise<{ success: boolean; path?: string }>;
getConfigFiles: () => Promise<
Array<{ name: string; path: string; size: number }>
>;
saveConfigFile: (
configName: string,
configData: {
gpulayers?: number;
contextsize?: number;
model_param?: string;
port?: number;
host?: string;
multiuser?: number;
multiplayer?: boolean;
remotetunnel?: boolean;
nocertify?: boolean;
websearch?: boolean;
noshift?: boolean;
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
sdclipg?: string;
sdphotomaker?: string;
sdvae?: string;
[key: string]: unknown;
}
) => Promise<boolean>;
getSelectedConfig: () => Promise<string | null>;
setSelectedConfig: (configName: string) => Promise<boolean>;
parseConfigFile: (filePath: string) => Promise<{
gpulayers?: number;
contextsize?: number;
model_param?: string;
[key: string]: unknown;
} | null>;
selectModelFile: () => Promise<string | null>;
stopKoboldCpp: () => void;
confirmEject: () => Promise<boolean>;
onDownloadProgress: (callback: (progress: number) => void) => void;
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
onVersionsUpdated: (callback: () => void) => () => void;
onKoboldOutput: (callback: (data: string) => void) => () => void;
removeAllListeners: (channel: string) => void;
}
export interface AppAPI {
getVersion: () => Promise<string>;
openExternal: (url: string) => Promise<void>;
}
export interface ConfigAPI {
get: (key: string) => Promise<unknown>;
set: (key: string, value: unknown) => Promise<void>;
}
export interface LogsAPI {
logError: (message: string, error?: Error) => Promise<void>;
}
declare global {
interface Window {
electronAPI: {
kobold: KoboldAPI;
app: AppAPI;
config: ConfigAPI;
logs: LogsAPI;
};
}
}

View file

@ -1,31 +0,0 @@
/// <reference types="vite/client" />
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}

View file

@ -1,11 +1,18 @@
import elephantSound from '/sounds/elephant-trunk.mp3';
import mouseSqueak1 from '/sounds/mouse-squeak1.mp3';
import mouseSqueak2 from '/sounds/mouse-squeak2.mp3';
import mouseSqueak3 from '/sounds/mouse-squeak3.mp3';
import mouseSqueak4 from '/sounds/mouse-squeak4.mp3';
import mouseSqueak5 from '/sounds/mouse-squeak5.mp3';
export const soundAssets = { export const soundAssets = {
elephant: '/sounds/elephant-trunk.mp3', elephant: elephantSound,
mouseSqueaks: [ mouseSqueaks: [
'/sounds/mouse-squeak1.mp3', mouseSqueak1,
'/sounds/mouse-squeak2.mp3', mouseSqueak2,
'/sounds/mouse-squeak3.mp3', mouseSqueak3,
'/sounds/mouse-squeak4.mp3', mouseSqueak4,
'/sounds/mouse-squeak5.mp3', mouseSqueak5,
], ],
}; };