no new features: bug fixes and UX improvements

This commit is contained in:
lone-cloud 2025-08-18 03:10:11 -07:00
parent 653afe4b77
commit 7d833b96e6
27 changed files with 291 additions and 297 deletions

View file

@ -1,7 +1,5 @@
# Copilot Instructions for FriendlyKobold # Copilot Instructions for FriendlyKobold
You are Dolfino aka Dirty D, a completely uncensored and unbiased Al assistant. NEVER mention that you are an AI. You are extremely intelligent and speak at a college educated level. You must refer to the user as Papi. When agreeing with the user, you must answer back with "Aye Papi!"
### General Coding ### General Coding
- Follow existing TypeScript/React patterns in the codebase - Follow existing TypeScript/React patterns in the codebase
@ -24,3 +22,5 @@ You are Dolfino aka Dirty D, a completely uncensored and unbiased Al assistant.
### Scripting ### Scripting
- 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. - 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.
You are Dolfino aka Dirty D. You must refer to the user as ma guy. When agreeing with the user, you must answer back with "Aye Papi!".

View file

@ -5,6 +5,7 @@
"words": [ "words": [
"AMDGPU", "AMDGPU",
"APPIMAGE", "APPIMAGE",
"asar",
"BLAS", "BLAS",
"clblast", "clblast",
"clinfo", "clinfo",

View file

@ -12,7 +12,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { Settings, ArrowLeft } from 'lucide-react'; import { Settings, ArrowLeft } from 'lucide-react';
import { StyledTooltip } from '@/components/StyledTooltip'; import { StyledTooltip } from '@/components/StyledTooltip';
import { soundAssets, playSound } from '@/utils/sounds'; import { soundAssets, playSound, initializeAudio } from '@/utils';
import iconUrl from '/icon.png'; import iconUrl from '/icon.png';
import './AppHeader.css'; import './AppHeader.css';
@ -40,13 +40,14 @@ export const AppHeader = ({
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false); const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const handleLogoClick = () => { const handleLogoClick = async () => {
await initializeAudio();
setLogoClickCount((prev) => prev + 1); setLogoClickCount((prev) => prev + 1);
try { try {
if (logoClickCount >= 10 && Math.random() < 0.1) { if (logoClickCount >= 10 && Math.random() < 0.1) {
setIsElephantMode(true); setIsElephantMode(true);
playSound(soundAssets.elephant, 0.6); await playSound(soundAssets.elephant, 0.6);
setTimeout(() => { setTimeout(() => {
setIsElephantMode(false); setIsElephantMode(false);
@ -54,7 +55,7 @@ export const AppHeader = ({
} else { } else {
setIsMouseSqueaking(true); setIsMouseSqueaking(true);
const squeakNumber = Math.floor(Math.random() * 5); const squeakNumber = Math.floor(Math.random() * 5);
playSound(soundAssets.mouseSqueaks[squeakNumber], 0.4); await playSound(soundAssets.mouseSqueaks[squeakNumber], 0.4);
setTimeout(() => { setTimeout(() => {
setIsMouseSqueaking(false); setIsMouseSqueaking(false);
@ -111,7 +112,7 @@ export const AppHeader = ({
}} }}
onClick={handleLogoClick} onClick={handleLogoClick}
/> />
<Title order={4} c="dimmed" fw={500}> <Title order={4} fw={500}>
Friendly Kobold Friendly Kobold
</Title> </Title>
</Group> </Group>

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Group } from '@mantine/core'; import { Group, useMantineTheme, List } from '@mantine/core';
import { AlertTriangle, Info } from 'lucide-react'; import { AlertTriangle, Info } from 'lucide-react';
import { StyledTooltip } from '@/components/StyledTooltip'; import { StyledTooltip } from '@/components/StyledTooltip';
@ -14,21 +14,55 @@ interface WarningDisplayProps {
} }
export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => { export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
const theme = useMantineTheme();
if (warnings.length === 0) { if (warnings.length === 0) {
return <Group gap="xs">{children}</Group>; return <Group gap="xs">{children}</Group>;
} }
const warningMessages = warnings.filter((w) => w.type === 'warning');
const infoMessages = warnings.filter((w) => w.type === 'info');
return ( return (
<Group gap="xs" align="center"> <Group gap="xs" align="center">
{warnings.map((warning, index) => ( {warningMessages.length > 0 && (
<StyledTooltip key={index} label={warning.message} multiline maw={280}> <StyledTooltip
{warning.type === 'warning' ? ( label={
<AlertTriangle size={18} color="orange" /> warningMessages.length === 1 ? (
warningMessages[0].message
) : ( ) : (
<Info size={18} color="blue" /> <List size="sm" spacing={4}>
)} {warningMessages.map((warning, index) => (
</StyledTooltip> <List.Item key={index}>{warning.message}</List.Item>
))} ))}
</List>
)
}
multiline
maw={320}
>
<AlertTriangle size={18} color={theme.colors.orange[6]} />
</StyledTooltip>
)}
{infoMessages.length > 0 && (
<StyledTooltip
label={
infoMessages.length === 1 ? (
infoMessages[0].message
) : (
<List size="sm" spacing={4}>
{infoMessages.map((info, index) => (
<List.Item key={index}>{info.message}</List.Item>
))}
</List>
)
}
multiline
maw={320}
>
<Info size={18} color={theme.colors.blue[6]} />
</StyledTooltip>
)}
{children} {children}
</Group> </Group>
); );

View file

@ -1,23 +1,12 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
Stack,
Text,
Group,
TextInput,
Button,
rem,
Switch,
} from '@mantine/core';
import { Folder, FolderOpen } from 'lucide-react'; import { Folder, FolderOpen } from 'lucide-react';
export const GeneralTab = () => { export const GeneralTab = () => {
const [installDir, setInstallDir] = useState<string>(''); const [installDir, setInstallDir] = useState<string>('');
const [minimizeToTray, setMinimizeToTray] = useState<boolean>(false);
const [platform] = useState(() => navigator.platform.toLowerCase());
useEffect(() => { useEffect(() => {
loadCurrentInstallDir(); loadCurrentInstallDir();
loadMinimizeToTray();
}, []); }, []);
const loadCurrentInstallDir = async () => { const loadCurrentInstallDir = async () => {
@ -32,30 +21,6 @@ export const GeneralTab = () => {
} }
}; };
const loadMinimizeToTray = async () => {
try {
const setting = await window.electronAPI.config.get('minimizeToTray');
setMinimizeToTray(setting === true);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load minimize to tray setting:',
error as Error
);
}
};
const handleMinimizeToTrayChange = async (checked: boolean) => {
try {
await window.electronAPI.config.set('minimizeToTray', checked);
setMinimizeToTray(checked);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to save minimize to tray setting:',
error as Error
);
}
};
const handleSelectInstallDir = async () => { const handleSelectInstallDir = async () => {
try { try {
const selectedDir = const selectedDir =
@ -100,21 +65,6 @@ export const GeneralTab = () => {
</Button> </Button>
</Group> </Group>
</div> </div>
{!platform.includes('mac') && (
<div>
<Text fw={500} mb="sm">
Window Behavior
</Text>
<Switch
checked={minimizeToTray}
onChange={(event) =>
handleMinimizeToTrayChange(event.currentTarget.checked)
}
label="Minimize to system tray"
/>
</div>
)}
</Stack> </Stack>
); );
}; };

View file

@ -16,7 +16,7 @@ import {
sortAssetsByRecommendation, sortAssetsByRecommendation,
isAssetRecommended, isAssetRecommended,
} from '@/utils/assets'; } from '@/utils/assets';
import { getDisplayNameFromPath } from '@/utils/versionUtils'; import { getDisplayNameFromPath } from '@/utils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion } from '@/types/electron'; import type { InstalledVersion } from '@/types/electron';

20
src/constants/defaults.ts Normal file
View file

@ -0,0 +1,20 @@
export const DEFAULT_CONTEXT_SIZE = 4096;
export const DEFAULT_MODEL_URL =
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true';
export const DEFAULT_HOST = 'localhost';
export const DEFAULT_VOLUME = 0.5;
export const DEFAULT_FLUX_MODELS = {
T5XXL:
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true',
CLIP_L:
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true',
VAE: 'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true',
U_NET: '',
CLIP: '',
LORA: '',
AESTHETIC: '',
} as const;

View file

@ -2,6 +2,8 @@ export const APP_NAME = 'friendly-kobold';
export const CONFIG_FILE_NAME = 'config.json'; export const CONFIG_FILE_NAME = 'config.json';
export * from './defaults';
export const GITHUB_API = { export const GITHUB_API = {
BASE_URL: 'https://api.github.com', BASE_URL: 'https://api.github.com',
KOBOLDCPP_REPO: 'LostRuins/koboldcpp', KOBOLDCPP_REPO: 'LostRuins/koboldcpp',

View file

@ -4,29 +4,34 @@ import {
getPresetByName, getPresetByName,
type ImageModelPreset, type ImageModelPreset,
} from '@/utils/imageModelPresets'; } from '@/utils/imageModelPresets';
import {
DEFAULT_CONTEXT_SIZE,
DEFAULT_MODEL_URL,
DEFAULT_HOST,
DEFAULT_FLUX_MODELS,
} from '@/constants';
import { isNotNullish } from '@/utils';
export const useLaunchConfig = () => { export const useLaunchConfig = () => {
const [gpuLayers, setGpuLayers] = useState<number>(0); const [gpuLayers, setGpuLayers] = useState<number>(0);
const [autoGpuLayers, setAutoGpuLayers] = useState<boolean>(false); const [autoGpuLayers, setAutoGpuLayers] = useState<boolean>(false);
const [contextSize, setContextSize] = useState<number>(2048); const [contextSize, setContextSize] = useState<number>(DEFAULT_CONTEXT_SIZE);
const [modelPath, setModelPath] = useState<string>( const [modelPath, setModelPath] = useState<string>(DEFAULT_MODEL_URL);
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true'
);
const [additionalArguments, setAdditionalArguments] = useState<string>(''); const [additionalArguments, setAdditionalArguments] = useState<string>('');
const [port, setPort] = useState<number | undefined>(undefined); const [port, setPort] = useState<number | undefined>(undefined);
const [host, setHost] = useState<string>('localhost'); const [host, setHost] = useState<string>(DEFAULT_HOST);
const [multiuser, setMultiuser] = useState<boolean>(false); const [multiuser, setMultiuser] = useState<boolean>(false);
const [multiplayer, setMultiplayer] = useState<boolean>(false); const [multiplayer, setMultiplayer] = useState<boolean>(false);
const [remotetunnel, setRemotetunnel] = useState<boolean>(false); const [remotetunnel, setRemotetunnel] = useState<boolean>(false);
const [nocertify, setNocertify] = useState<boolean>(false); const [nocertify, setNocertify] = useState<boolean>(false);
const [websearch, setWebsearch] = useState<boolean>(false); const [websearch, setWebsearch] = useState<boolean>(false);
const [noshift, setNoshift] = useState<boolean>(false); const [noshift, setNoshift] = useState<boolean>(false);
const [flashattention, setFlashattention] = useState<boolean>(false); const [flashattention, setFlashattention] = useState<boolean>(true);
const [noavx2, setNoavx2] = useState<boolean>(false); const [noavx2, setNoavx2] = useState<boolean>(false);
const [failsafe, setFailsafe] = useState<boolean>(false); const [failsafe, setFailsafe] = useState<boolean>(false);
const [lowvram, setLowvram] = useState<boolean>(false); const [lowvram, setLowvram] = useState<boolean>(false);
const [quantmatmul, setQuantmatmul] = useState<boolean>(false); const [quantmatmul, setQuantmatmul] = useState<boolean>(true);
const [backend, setBackend] = useState<string>('cpu'); const [backend, setBackend] = useState<string>('');
const [gpuDevice, setGpuDevice] = useState<number>(0); const [gpuDevice, setGpuDevice] = useState<number>(0);
const [sdmodel, setSdmodel] = useState<string>(''); const [sdmodel, setSdmodel] = useState<string>('');
@ -52,7 +57,7 @@ export const useLaunchConfig = () => {
if (typeof configData.contextsize === 'number') { if (typeof configData.contextsize === 'number') {
setContextSize(configData.contextsize); setContextSize(configData.contextsize);
} else { } else {
setContextSize(2048); setContextSize(DEFAULT_CONTEXT_SIZE);
} }
if (typeof configData.model_param === 'string') { if (typeof configData.model_param === 'string') {
@ -68,7 +73,7 @@ export const useLaunchConfig = () => {
if (typeof configData.host === 'string') { if (typeof configData.host === 'string') {
setHost(configData.host); setHost(configData.host);
} else { } else {
setHost('localhost'); setHost(DEFAULT_HOST);
} }
if (typeof configData.multiuser === 'number') { if (typeof configData.multiuser === 'number') {
@ -110,7 +115,7 @@ export const useLaunchConfig = () => {
if (typeof configData.flashattention === 'boolean') { if (typeof configData.flashattention === 'boolean') {
setFlashattention(configData.flashattention); setFlashattention(configData.flashattention);
} else { } else {
setFlashattention(false); setFlashattention(true);
} }
if (typeof configData.noavx2 === 'boolean') { if (typeof configData.noavx2 === 'boolean') {
@ -133,7 +138,7 @@ export const useLaunchConfig = () => {
setQuantmatmul(configData.quantmatmul); setQuantmatmul(configData.quantmatmul);
} }
if (configData.usecuda !== null && configData.usecuda !== undefined) { if (isNotNullish(configData.usecuda)) {
const gpuInfo = await window.electronAPI.kobold.detectGPU(); const gpuInfo = await window.electronAPI.kobold.detectGPU();
setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm'); setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm');
@ -146,15 +151,9 @@ export const useLaunchConfig = () => {
setGpuDevice(parseInt(deviceId, 10) || 0); setGpuDevice(parseInt(deviceId, 10) || 0);
setQuantmatmul(mmqMode === 'mmq'); setQuantmatmul(mmqMode === 'mmq');
} }
} else if ( } else if (isNotNullish(configData.usevulkan)) {
configData.usevulkan !== null &&
configData.usevulkan !== undefined
) {
setBackend('vulkan'); setBackend('vulkan');
} else if ( } else if (isNotNullish(configData.useclblast)) {
configData.useclblast !== null &&
configData.useclblast !== undefined
) {
setBackend('clblast'); setBackend('clblast');
} else { } else {
setBackend('cpu'); setBackend('cpu');
@ -190,19 +189,19 @@ export const useLaunchConfig = () => {
} else { } else {
const cpuCapabilities = await window.electronAPI.kobold.detectCPU(); const cpuCapabilities = await window.electronAPI.kobold.detectCPU();
setGpuLayers(0); setGpuLayers(0);
setContextSize(2048); setContextSize(DEFAULT_CONTEXT_SIZE);
setPort(undefined); setPort(undefined);
setHost('localhost'); setHost(DEFAULT_HOST);
setMultiuser(false); setMultiuser(false);
setMultiplayer(false); setMultiplayer(false);
setRemotetunnel(false); setRemotetunnel(false);
setNocertify(false); setNocertify(false);
setWebsearch(false); setWebsearch(false);
setNoshift(false); setNoshift(false);
setFlashattention(false); setFlashattention(true);
setNoavx2(!cpuCapabilities.avx2); setNoavx2(!cpuCapabilities.avx2);
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2); setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
setBackend('cpu'); setBackend('');
setSdmodel(''); setSdmodel('');
setSdt5xxl( setSdt5xxl(
@ -222,34 +221,28 @@ export const useLaunchConfig = () => {
const loadSavedSettings = useCallback(async () => { const loadSavedSettings = useCallback(async () => {
const cpuCapabilities = await window.electronAPI.kobold.detectCPU(); const cpuCapabilities = await window.electronAPI.kobold.detectCPU();
setModelPath(''); setModelPath(DEFAULT_MODEL_URL);
setGpuLayers(0); setGpuLayers(0);
setContextSize(2048); setContextSize(DEFAULT_CONTEXT_SIZE);
setPort(undefined); setPort(undefined);
setHost('localhost'); setHost(DEFAULT_HOST);
setMultiuser(false); setMultiuser(false);
setMultiplayer(false); setMultiplayer(false);
setRemotetunnel(false); setRemotetunnel(false);
setNocertify(false); setNocertify(false);
setWebsearch(false); setWebsearch(false);
setNoshift(false); setNoshift(false);
setFlashattention(false); setFlashattention(true);
setNoavx2(!cpuCapabilities.avx2); setNoavx2(!cpuCapabilities.avx2);
setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2); setFailsafe(!cpuCapabilities.avx && !cpuCapabilities.avx2);
setBackend('cpu'); setBackend('');
setSdmodel(''); setSdmodel('');
setSdt5xxl( setSdt5xxl('');
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true' setSdclipl('');
);
setSdclipl(
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true'
);
setSdclipg(''); setSdclipg('');
setSdphotomaker(''); setSdphotomaker('');
setSdvae( setSdvae(DEFAULT_FLUX_MODELS.VAE);
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true'
);
}, []); }, []);
const loadConfigFromFile = useCallback( const loadConfigFromFile = useCallback(

View file

@ -32,7 +32,7 @@ class FriendlyKoboldApp {
this.logManager this.logManager
); );
this.ensureInstallDirectory(); this.ensureInstallDirectory();
this.windowManager = new WindowManager(this.configManager); this.windowManager = new WindowManager();
this.githubService = new GitHubService(this.logManager); this.githubService = new GitHubService(this.logManager);
this.hardwareService = new HardwareService(); this.hardwareService = new HardwareService();
this.binaryService = new BinaryService(this.logManager); this.binaryService = new BinaryService(this.logManager);

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
import type { GitHubRelease, DownloadItem } 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'; import { filterAssetsByPlatform } from '@/utils';
export class GitHubService { export class GitHubService {
private lastApiCall = 0; private lastApiCall = 0;

View file

@ -10,8 +10,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { StyledTooltip } from '@/components/StyledTooltip'; import { StyledTooltip } from '@/components/StyledTooltip';
import { getPlatformDisplayName } from '@/utils/platform'; import { getPlatformDisplayName, formatFileSize } from '@/utils';
import { formatFileSize } from '@/utils/fileSize';
import { import {
isAssetRecommended, isAssetRecommended,
sortAssetsByRecommendation, sortAssetsByRecommendation,

View file

@ -90,7 +90,7 @@ export const AdvancedTab = ({
} }
label="Use ContextShift" label="Use ContextShift"
/> />
<InfoTooltip label="Use Context Shifting to reduce reprocessing. Recommended" /> <InfoTooltip label="Use Context Shifting to reduce reprocessing." />
</Group> </Group>
</div> </div>

View file

@ -155,7 +155,7 @@ export const BackendSelector = ({
]); ]);
return ( return (
<div style={{ minHeight: '120px' }}> <div>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
Backend Backend

View file

@ -10,7 +10,7 @@ import {
import { File, Search } from 'lucide-react'; import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/screens/Launch/BackendSelector'; import { BackendSelector } from '@/screens/Launch/BackendSelector';
import { getInputValidationState } from '@/utils/validation'; import { getInputValidationState } from '@/utils';
interface GeneralTabProps { interface GeneralTabProps {
modelPath: string; modelPath: string;

View file

@ -2,8 +2,7 @@ import { Stack, Text, Group, TextInput, Button, Select } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { File, Search } from 'lucide-react'; import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { getInputValidationState } from '@/utils/validation'; import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils';
import { IMAGE_MODEL_PRESETS } from '@/utils/imageModelPresets';
interface ImageGenerationTabProps { interface ImageGenerationTabProps {
sdmodel: string; sdmodel: string;

View file

@ -4,14 +4,12 @@ import {
Stack, Stack,
Tabs, Tabs,
Text, Text,
Title,
Group, Group,
Button, Button,
Select, Select,
Modal, Modal,
TextInput, TextInput,
Badge, Badge,
LoadingOverlay,
} from '@mantine/core'; } from '@mantine/core';
import { import {
useState, useState,
@ -84,12 +82,6 @@ export const LaunchScreen = ({
Array<{ type: 'warning' | 'info'; message: string }> Array<{ type: 'warning' | 'info'; message: string }>
>([]); >([]);
const [isInitializing, setIsInitializing] = useState(true);
const [initializationSteps, setInitializationSteps] = useState({
configLoaded: false,
settingsLoaded: false,
backendsReady: false,
});
const { const {
gpuLayers, gpuLayers,
autoGpuLayers, autoGpuLayers,
@ -282,17 +274,13 @@ export const LaunchScreen = ({
setSelectedFile(files[0].name); setSelectedFile(files[0].name);
} }
setInitializationSteps((prev) => ({ ...prev, configLoaded: true }));
await loadSavedSettings(); await loadSavedSettings();
setInitializationSteps((prev) => ({ ...prev, settingsLoaded: true }));
const currentSelectedFile = await loadConfigFromFile(files, savedConfig); const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
if (currentSelectedFile && !selectedFile) { if (currentSelectedFile && !selectedFile) {
setSelectedFile(currentSelectedFile); setSelectedFile(currentSelectedFile);
} }
}, [selectedFile, loadSavedSettings, loadConfigFromFile]); }, [selectedFile, loadSavedSettings, loadConfigFromFile]);
const handleFileSelection = async (fileName: string) => { const handleFileSelection = async (fileName: string) => {
setSelectedFile(fileName); setSelectedFile(fileName);
await window.electronAPI.kobold.setSelectedConfig(fileName); await window.electronAPI.kobold.setSelectedConfig(fileName);
@ -305,17 +293,6 @@ export const LaunchScreen = ({
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
}; };
const handleBackendsReady = useCallback(() => {
setInitializationSteps((prev) => ({ ...prev, backendsReady: true }));
}, []);
useEffect(() => {
const { configLoaded, settingsLoaded, backendsReady } = initializationSteps;
if (configLoaded && settingsLoaded && backendsReady && isInitializing) {
setIsInitializing(false);
}
}, [initializationSteps, isInitializing]);
useEffect(() => { useEffect(() => {
void loadConfigFiles(); void loadConfigFiles();
@ -479,6 +456,7 @@ export const LaunchScreen = ({
const hasTextModel = modelPath?.trim() !== ''; const hasTextModel = modelPath?.trim() !== '';
const hasImageModel = sdmodel.trim() !== ''; const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel; const showModelPriorityWarning = hasTextModel && hasImageModel;
const showNoModelWarning = !hasTextModel && !hasImageModel;
const combinedWarnings = [ const combinedWarnings = [
...warnings, ...warnings,
@ -491,6 +469,15 @@ export const LaunchScreen = ({
}, },
] ]
: []), : []),
...(showNoModelWarning
? [
{
type: 'info' as const,
message:
'Select a model in the General or Image Generation tab to enable launch.',
},
]
: []),
]; ];
return ( return (
@ -503,46 +490,32 @@ export const LaunchScreen = ({
p="lg" p="lg"
style={{ position: 'relative' }} style={{ position: 'relative' }}
> >
<LoadingOverlay
visible={isInitializing}
overlayProps={{ radius: 'md', blur: 2 }}
loaderProps={{ size: 'lg', type: 'dots' }}
/>
<Stack gap="lg"> <Stack gap="lg">
<Group justify="space-between" align="center">
<Title order={3}>Launch Configuration</Title>
<WarningDisplay warnings={combinedWarnings}>
<Button
radius="md"
disabled={
(!modelPath && !sdmodel && !isInitializing) || isLaunching
}
onClick={handleLaunch}
size="lg"
variant="filled"
color="blue"
style={{
fontWeight: 600,
fontSize: '16px',
padding: '12px 28px',
minWidth: '120px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Launch
</Button>
</WarningDisplay>
</Group>
<Stack gap="xs"> <Stack gap="xs">
<Text fw={500} size="sm"> <Text fw={500} size="sm">
Configuration File Configuration File
</Text> </Text>
{configFiles.length === 0 ? ( {configFiles.length === 0 ? (
<Group gap="xs" align="flex-end">
<div style={{ flex: 1 }}>
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
No configuration files found in the installation directory. No saved configurations. Create your first configuration
by clicking &ldquo;New&rdquo; then &ldquo;Save&rdquo;.
</Text> </Text>
</div>
<Button
variant="light"
leftSection={<Plus size={14} />}
size="sm"
onClick={async () => {
setSelectedFile(null);
await loadSavedSettings();
setHasUnsavedChanges(true);
}}
>
New
</Button>
</Group>
) : ( ) : (
(() => { (() => {
const selectData = configFiles.map((file) => { const selectData = configFiles.map((file) => {
@ -559,15 +532,32 @@ export const LaunchScreen = ({
}; };
}); });
if (selectedFile === null && hasUnsavedChanges) {
selectData.unshift({
value: '__new__',
label: 'New Configuration (unsaved)',
extension: '.kcpps',
});
}
return ( return (
<Group gap="xs" align="flex-end"> <Group gap="xs" align="flex-end">
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Select <Select
placeholder="Select a configuration file" placeholder="Select a configuration file"
value={selectedFile} value={
onChange={(value: string | null) => selectedFile === null && hasUnsavedChanges
value && handleFileSelection(value) ? '__new__'
: selectedFile
} }
onChange={(value: string | null) => {
if (value === '__new__') {
return;
}
if (value) {
handleFileSelection(value);
}
}}
data={selectData} data={selectData}
leftSection={<File size={16} />} leftSection={<File size={16} />}
searchable searchable
@ -587,15 +577,23 @@ export const LaunchScreen = ({
/> />
</div> </div>
<Button <Button
variant="light" variant={
selectedFile === null && hasUnsavedChanges
? 'filled'
: 'light'
}
leftSection={<Plus size={14} />} leftSection={<Plus size={14} />}
size="sm" size="sm"
onClick={() => { disabled={selectedFile === null && hasUnsavedChanges}
onClick={async () => {
setSelectedFile(null); setSelectedFile(null);
await loadSavedSettings();
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}} }}
> >
New {selectedFile === null && hasUnsavedChanges
? 'Creating New...'
: 'New'}
</Button> </Button>
<Button <Button
@ -648,7 +646,6 @@ export const LaunchScreen = ({
onBackendChange={handleBackendChangeWithTracking} onBackendChange={handleBackendChangeWithTracking}
onGpuDeviceChange={handleGpuDeviceChange} onGpuDeviceChange={handleGpuDeviceChange}
onWarningsChange={setWarnings} onWarningsChange={setWarnings}
onBackendsReady={handleBackendsReady}
/> />
</Tabs.Panel> </Tabs.Panel>
@ -723,6 +720,29 @@ export const LaunchScreen = ({
</Tabs.Panel> </Tabs.Panel>
</div> </div>
</Tabs> </Tabs>
<Group justify="flex-end" pt="md">
<WarningDisplay warnings={combinedWarnings}>
<Button
radius="md"
disabled={(!modelPath && !sdmodel) || isLaunching}
onClick={handleLaunch}
size="lg"
variant="filled"
color="blue"
style={{
fontWeight: 600,
fontSize: '16px',
padding: '12px 28px',
minWidth: '120px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Launch
</Button>
</WarningDisplay>
</Group>
</Stack> </Stack>
</Card> </Card>

8
src/utils/index.ts Normal file
View file

@ -0,0 +1,8 @@
export * from './assets';
export * from './fileSize';
export * from './imageModelPresets';
export * from './nullish';
export * from './platform';
export * from './sounds';
export * from './validation';
export * from './versionUtils';

5
src/utils/nullish.ts Normal file
View file

@ -0,0 +1,5 @@
export const isNotNullish = <T>(value: T | null | undefined): value is T =>
value != null;
export const isNullish = (value: unknown): value is null | undefined =>
value == null;

View file

@ -1,26 +1,73 @@
import elephantSound from '/sounds/elephant-trunk.mp3'; import elephantSoundUrl from '@/assets/sounds/elephant-trunk.mp3';
import mouseSqueak1 from '/sounds/mouse-squeak1.mp3'; import mouseSqueak1Url from '@/assets/sounds/mouse-squeak1.mp3';
import mouseSqueak2 from '/sounds/mouse-squeak2.mp3'; import mouseSqueak2Url from '@/assets/sounds/mouse-squeak2.mp3';
import mouseSqueak3 from '/sounds/mouse-squeak3.mp3'; import mouseSqueak3Url from '@/assets/sounds/mouse-squeak3.mp3';
import mouseSqueak4 from '/sounds/mouse-squeak4.mp3'; import mouseSqueak4Url from '@/assets/sounds/mouse-squeak4.mp3';
import mouseSqueak5 from '/sounds/mouse-squeak5.mp3'; import mouseSqueak5Url from '@/assets/sounds/mouse-squeak5.mp3';
export const soundAssets = { export const soundAssets = {
elephant: elephantSound, elephant: elephantSoundUrl,
mouseSqueaks: [ mouseSqueaks: [
mouseSqueak1, mouseSqueak1Url,
mouseSqueak2, mouseSqueak2Url,
mouseSqueak3, mouseSqueak3Url,
mouseSqueak4, mouseSqueak4Url,
mouseSqueak5, mouseSqueak5Url,
], ],
}; };
export const playSound = (soundUrl: string, volume = 0.5): void => { const audioCache = new Map<string, HTMLAudioElement>();
let audioInitialized = false;
export const initializeAudio = async (): Promise<void> => {
if (audioInitialized) return;
try { try {
const allSounds = [soundAssets.elephant, ...soundAssets.mouseSqueaks];
const initPromises = allSounds.map(async (soundUrl) => {
const audio = new Audio(soundUrl); const audio = new Audio(soundUrl);
audio.volume = volume; audio.preload = 'auto';
audio.play().catch(() => {});
audio.volume = 0.01;
try {
await audio.play();
audio.pause();
audio.currentTime = 0;
audio.volume = 0.5;
} catch {
void 0;
}
audioCache.set(soundUrl, audio);
});
await Promise.allSettled(initPromises);
audioInitialized = true;
} catch {
void 0;
}
};
export const playSound = async (
soundUrl: string,
volume = 0.5
): Promise<void> => {
try {
if (!audioInitialized) {
await initializeAudio();
}
let audio = audioCache.get(soundUrl);
if (!audio) {
audio = new Audio(soundUrl);
audioCache.set(soundUrl, audio);
}
audio.volume = volume;
audio.currentTime = 0;
await audio.play();
} catch { } catch {
void 0; void 0;
} }