refactor code for simplicity, tunnel custom frontend server

This commit is contained in:
Egor 2025-12-02 23:37:51 -08:00
parent d39a26477a
commit 0576b46b29
23 changed files with 391 additions and 618 deletions

View file

@ -9,20 +9,20 @@ interface AppRouterProps {
currentScreen: Screen | null;
hasInitialized: boolean;
activeInterfaceTab: InterfaceTab;
isServerReady: boolean;
onWelcomeComplete: () => void;
onDownloadComplete: () => void;
onLaunch: () => void;
onTabChange: (tab: InterfaceTab) => void;
}
export const AppRouter = ({
currentScreen,
hasInitialized,
activeInterfaceTab,
isServerReady,
onWelcomeComplete,
onDownloadComplete,
onLaunch,
onTabChange,
}: AppRouterProps) => {
const isInterfaceScreen = currentScreen === 'interface';
@ -55,7 +55,7 @@ export const AppRouter = ({
>
<InterfaceScreen
activeTab={activeInterfaceTab}
onTabChange={onTabChange}
isServerReady={isServerReady}
/>
</ScreenTransition>
</>

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import {
AppShell,
Loader,
@ -19,6 +19,7 @@ import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { usePreferencesStore } from '@/stores/preferences';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { getDefaultInterfaceTab } from '@/utils/interface';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab, Screen } from '@/types';
@ -29,14 +30,45 @@ export const App = () => {
const [hasInitialized, setHasInitialized] = useState(false);
const [activeInterfaceTab, setActiveInterfaceTab] =
useState<InterfaceTab>('terminal');
const [isServerReady, setIsServerReady] = useState(false);
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null);
const isInterfaceScreen = currentScreen === 'interface';
const { resolvedColorScheme: appColorScheme, systemMonitoringEnabled } =
usePreferencesStore();
const {
resolvedColorScheme: appColorScheme,
systemMonitoringEnabled,
frontendPreference,
imageGenerationFrontendPreference,
} = usePreferencesStore();
const { setColorScheme } = useMantineColorScheme();
const { model, sdmodel } = useLaunchConfigStore();
const { model, sdmodel, isTextMode, isImageGenerationMode } =
useLaunchConfigStore();
const defaultInterfaceTab = useMemo(
() =>
getDefaultInterfaceTab({
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
}),
[
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
]
);
useEffect(() => {
const cleanup = window.electronAPI.kobold.onServerReady(() => {
setIsServerReady(true);
setActiveInterfaceTab(defaultInterfaceTab);
});
return cleanup;
}, [defaultInterfaceTab]);
useEffect(() => {
setColorScheme(appColorScheme);
@ -63,6 +95,8 @@ export const App = () => {
const performEject = () => {
window.electronAPI.kobold.stopKoboldCpp();
setIsServerReady(false);
setActiveInterfaceTab('terminal');
setCurrentScreen('launch');
};
@ -235,10 +269,10 @@ export const App = () => {
currentScreen={currentScreen}
hasInitialized={hasInitialized}
activeInterfaceTab={activeInterfaceTab}
isServerReady={isServerReady}
onWelcomeComplete={handleWelcomeComplete}
onDownloadComplete={handleDownloadComplete}
onLaunch={handleLaunch}
onTabChange={setActiveInterfaceTab}
/>
)}
</ErrorBoundary>

View file

@ -7,31 +7,16 @@ import {
} from 'react';
import { Box, ScrollArea, ActionIcon } from '@mantine/core';
import { ChevronDown } from 'lucide-react';
import {
SERVER_READY_SIGNALS,
STATUSBAR_HEIGHT,
TITLEBAR_HEIGHT,
} from '@/constants';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences';
interface TerminalTabProps {
onServerReady: () => void;
}
export interface TerminalTabRef {
scrollToBottom: () => void;
}
export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
({ onServerReady }, ref) => {
const { isImageGenerationMode, isTextMode } = useLaunchConfigStore();
const {
frontendPreference,
imageGenerationFrontendPreference,
resolvedColorScheme: colorScheme,
} = usePreferencesStore();
export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
const [terminalContent, setTerminalContent] = useState('');
const [isUserScrolling, setIsUserScrolling] = useState(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
@ -73,44 +58,12 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
}, [terminalContent, shouldAutoScroll, isUserScrolling]);
useEffect(() => {
const cleanup = window.electronAPI.kobold.onKoboldOutput(
(data: string) => {
setTerminalContent((prev) => {
const newData = data.toString();
if (onServerReady) {
const effectiveFrontend = isTextMode
? frontendPreference
: imageGenerationFrontendPreference === 'builtin'
? 'koboldcpp'
: frontendPreference;
let signalToCheck: string = SERVER_READY_SIGNALS.KOBOLDCPP;
if (effectiveFrontend === 'sillytavern') {
signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN;
} else if (effectiveFrontend === 'openwebui') {
signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI;
}
if (newData.includes(signalToCheck)) {
setTimeout(() => onServerReady(), 3000);
}
}
return handleTerminalOutput(prev, newData);
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
setTerminalContent((prev) => handleTerminalOutput(prev, data.toString()));
});
}
);
return cleanup;
}, [
onServerReady,
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode,
isTextMode,
]);
}, []);
const scrollToBottom = () => {
if (viewportRef.current) {
@ -197,7 +150,6 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
)}
</Box>
);
}
);
});
TerminalTab.displayName = 'TerminalTab';

View file

@ -1,54 +1,22 @@
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useRef, useEffect } from 'react';
import { ServerTab } from '@/components/screens/Interface/ServerTab';
import {
TerminalTab,
type TerminalTabRef,
} from '@/components/screens/Interface/TerminalTab';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences';
import { getDefaultInterfaceTab } from '@/utils/interface';
import type { InterfaceTab } from '@/types';
interface InterfaceScreenProps {
activeTab?: InterfaceTab | null;
onTabChange?: (tab: InterfaceTab) => void;
isServerReady: boolean;
}
export const InterfaceScreen = ({
activeTab,
onTabChange,
isServerReady,
}: InterfaceScreenProps) => {
const [isServerReady, setIsServerReady] = useState(false);
const terminalTabRef = useRef<TerminalTabRef>(null);
const { isTextMode, isImageGenerationMode } = useLaunchConfigStore();
const { frontendPreference, imageGenerationFrontendPreference } =
usePreferencesStore();
const defaultInterfaceTab = useMemo(
() =>
getDefaultInterfaceTab({
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
}),
[
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
]
);
const handleServerReady = useCallback(() => {
setIsServerReady(true);
if (onTabChange) {
onTabChange(defaultInterfaceTab);
}
}, [onTabChange, defaultInterfaceTab]);
useEffect(() => {
if (activeTab === 'terminal' && terminalTabRef.current) {
terminalTabRef.current.scrollToBottom();
@ -84,7 +52,7 @@ export const InterfaceScreen = ({
display: activeTab === 'terminal' ? 'block' : 'none',
}}
>
<TerminalTab ref={terminalTabRef} onServerReady={handleServerReady} />
<TerminalTab ref={terminalTabRef} />
</div>
</div>
);

View file

@ -13,7 +13,7 @@ import { Plus, Trash2 } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
export const AdvancedTab = () => {
const {
@ -30,19 +30,19 @@ export const AdvancedTab = () => {
backend,
moecpu,
moeexperts,
handleAdditionalArgumentsChange,
handlePreLaunchCommandsChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleUsemmapChange,
handleDebugmodeChange,
handleMoecpuChange,
handleMoeexpertsChange,
} = useLaunchConfig();
setAdditionalArguments,
setPreLaunchCommands,
setNoshift,
setFlashattention,
setNoavx2,
setFailsafe,
setLowvram,
setQuantmatmul,
setUsemmap,
setDebugmode,
setMoecpu,
setMoeexperts,
} = useLaunchConfigStore();
const [commandLineModalOpen, setCommandLineModalOpen] = useState(false);
const [backendSupport, setBackendSupport] = useState<{
noavx2: boolean;
@ -55,7 +55,7 @@ export const AdvancedTab = () => {
const updatedArgs = currentArgs
? `${currentArgs} ${newArgument}`
: newArgument;
handleAdditionalArgumentsChange(updatedArgs);
setAdditionalArguments(updatedArgs);
};
const isGpuBackend = backend === 'cuda' || backend === 'rocm';
@ -86,21 +86,21 @@ export const AdvancedTab = () => {
<SimpleGrid cols={3} spacing="lg" verticalSpacing="md">
<CheckboxWithTooltip
checked={!noshift}
onChange={(checked) => handleNoshiftChange(!checked)}
onChange={(checked) => setNoshift(!checked)}
label="Context Shift"
tooltip="Use Context Shifting to reduce reprocessing."
/>
<CheckboxWithTooltip
checked={noshift}
onChange={handleNoshiftChange}
onChange={setNoshift}
label="No Shift"
tooltip="Don't use GPU layer shifting for incomplete offloads, which may reduce model performance."
/>
<CheckboxWithTooltip
checked={noavx2}
onChange={handleNoavx2Change}
onChange={setNoavx2}
label="Disable AVX2"
tooltip={
!backendSupport?.noavx2 && !isLoading
@ -112,14 +112,14 @@ export const AdvancedTab = () => {
<CheckboxWithTooltip
checked={usemmap}
onChange={handleUsemmapChange}
onChange={setUsemmap}
label="MMAP"
tooltip="Use MMAP to load models when enabled."
/>
<CheckboxWithTooltip
checked={quantmatmul && isGpuBackend}
onChange={handleQuantmatmulChange}
onChange={setQuantmatmul}
label="QuantMatMul"
tooltip={
!isGpuBackend
@ -131,7 +131,7 @@ export const AdvancedTab = () => {
<CheckboxWithTooltip
checked={failsafe}
onChange={handleFailsafeChange}
onChange={setFailsafe}
label="Failsafe"
tooltip={
!backendSupport?.failsafe && !isLoading
@ -143,14 +143,14 @@ export const AdvancedTab = () => {
<CheckboxWithTooltip
checked={flashattention}
onChange={handleFlashattentionChange}
onChange={setFlashattention}
label="Flash Attention"
tooltip="Enable flash attention to reduce memory usage. May produce incorrect answers for some prompts, but improves performance."
/>
<CheckboxWithTooltip
checked={lowvram && isGpuBackend}
onChange={handleLowvramChange}
onChange={setLowvram}
label="Low VRAM"
tooltip={
!isGpuBackend
@ -162,7 +162,7 @@ export const AdvancedTab = () => {
<CheckboxWithTooltip
checked={debugmode}
onChange={handleDebugmodeChange}
onChange={setDebugmode}
label="Debug Mode"
tooltip="Shows additional debug info in the terminal."
/>
@ -181,7 +181,7 @@ export const AdvancedTab = () => {
</Group>
<NumberInput
value={moeexperts}
onChange={(value) => handleMoeexpertsChange(Number(value))}
onChange={(value) => setMoeexperts(Number(value))}
min={-1}
max={128}
step={1}
@ -198,7 +198,7 @@ export const AdvancedTab = () => {
</Group>
<NumberInput
value={moecpu}
onChange={(value) => handleMoecpuChange(Number(value) || 0)}
onChange={(value) => setMoecpu(Number(value) || 0)}
min={0}
max={999}
step={1}
@ -229,7 +229,7 @@ export const AdvancedTab = () => {
placeholder="Additional command line arguments"
value={additionalArguments}
onChange={(event) =>
handleAdditionalArgumentsChange(event.currentTarget.value)
setAdditionalArguments(event.currentTarget.value)
}
/>
</div>
@ -250,7 +250,7 @@ export const AdvancedTab = () => {
onChange={(event) => {
const newCommands = [...preLaunchCommands];
newCommands[index] = event.currentTarget.value;
handlePreLaunchCommandsChange(newCommands);
setPreLaunchCommands(newCommands);
}}
style={{ flex: 1 }}
/>
@ -262,7 +262,7 @@ export const AdvancedTab = () => {
const newCommands = preLaunchCommands.filter(
(_, i) => i !== index
);
handlePreLaunchCommandsChange(
setPreLaunchCommands(
newCommands.length === 0 ? [''] : newCommands
);
}}
@ -276,7 +276,7 @@ export const AdvancedTab = () => {
size="xs"
leftSection={<Plus size={14} />}
onClick={() => {
handlePreLaunchCommandsChange([...preLaunchCommands, '']);
setPreLaunchCommands([...preLaunchCommands, '']);
}}
style={{ alignSelf: 'flex-start' }}
>

View file

@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { AccelerationSelectItem } from '@/components/screens/Launch/GeneralTab/AccelerationSelectItem';
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { AccelerationOption } from '@/types';
import { Select } from '@/components/Select';
@ -16,10 +16,10 @@ export const AccelerationSelector = () => {
contextSize,
gpuDeviceSelection,
flashattention,
handleBackendChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
} = useLaunchConfig();
setBackend,
setGpuLayers,
setAutoGpuLayers,
} = useLaunchConfigStore();
const [availableAccelerations, setAvailableAccelerations] = useState<
AccelerationOption[]
@ -67,11 +67,11 @@ export const AccelerationSelector = () => {
(a) => !a.disabled
);
if (fallbackAcceleration) {
handleBackendChange(fallbackAcceleration.value);
setBackend(fallbackAcceleration.value);
}
}
}
}, [availableAccelerations, backend, handleBackendChange]);
}, [availableAccelerations, backend, setBackend]);
useEffect(() => {
const calculateLayers = async () => {
@ -121,7 +121,7 @@ export const AccelerationSelector = () => {
flashattention
);
handleGpuLayersChange(result.recommendedLayers);
setGpuLayers(result.recommendedLayers);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to calculate optimal GPU layers',
@ -142,7 +142,7 @@ export const AccelerationSelector = () => {
flashattention,
isLoadingAccelerations,
isMac,
handleGpuLayersChange,
setGpuLayers,
]);
return (
@ -170,7 +170,7 @@ export const AccelerationSelector = () => {
}
onChange={(value) => {
if (value) {
handleBackendChange(value);
setBackend(value);
}
}}
data={availableAccelerations.map((a) => ({
@ -215,7 +215,7 @@ export const AccelerationSelector = () => {
: undefined
}
onChange={(event) =>
handleGpuLayersChange(Number(event.target.value) || 0)
setGpuLayers(Number(event.target.value) || 0)
}
type="number"
min={0}
@ -230,7 +230,7 @@ export const AccelerationSelector = () => {
label="Auto"
checked={autoGpuLayers}
onChange={(event) =>
handleAutoGpuLayersChange(event.currentTarget.checked)
setAutoGpuLayers(event.currentTarget.checked)
}
size="sm"
disabled={backend === 'cpu' && !isMac}

View file

@ -1,9 +1,12 @@
import { Text, Group, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { Select } from '@/components/Select';
import type { AccelerationOption } from '@/types';
const GPU_BACKENDS = ['cuda', 'rocm', 'vulkan', 'clblast'];
const TENSOR_SPLIT_BACKENDS = ['cuda', 'rocm', 'vulkan'];
interface GpuDeviceSelectorProps {
availableAccelerations: AccelerationOption[];
}
@ -15,18 +18,14 @@ export const GpuDeviceSelector = ({
backend,
gpuDeviceSelection,
tensorSplit,
handleGpuDeviceSelectionChange,
handleTensorSplitChange,
} = useLaunchConfig();
setGpuDeviceSelection,
setTensorSplit,
} = useLaunchConfigStore();
const selectedAcceleration = availableAccelerations.find(
(a) => a.value === backend
);
const isGpuAcceleration =
backend === 'cuda' ||
backend === 'rocm' ||
backend === 'vulkan' ||
backend === 'clblast';
const isGpu = GPU_BACKENDS.includes(backend);
const getDiscreteDeviceCount = () => {
if (!selectedAcceleration?.devices) return 0;
@ -40,11 +39,11 @@ export const GpuDeviceSelector = ({
const hasMultipleDevices = getDiscreteDeviceCount() > 1;
const showTensorSplit =
(backend === 'cuda' || backend === 'rocm' || backend === 'vulkan') &&
TENSOR_SPLIT_BACKENDS.includes(backend) &&
hasMultipleDevices &&
gpuDeviceSelection === 'all';
if (!isGpuAcceleration || !hasMultipleDevices) {
if (!isGpu || !hasMultipleDevices) {
return null;
}
@ -119,7 +118,7 @@ export const GpuDeviceSelector = ({
value={gpuDeviceSelection}
onChange={(value) => {
if (value) {
handleGpuDeviceSelectionChange(value);
setGpuDeviceSelection(value);
}
}}
data={deviceOptions}
@ -138,9 +137,7 @@ export const GpuDeviceSelector = ({
<TextInput
placeholder="e.g., 3,2 or 1,1,1"
value={tensorSplit}
onChange={(event) =>
handleTensorSplitChange(event.target.value)
}
onChange={(event) => setTensorSplit(event.target.value)}
size="sm"
/>
</>

View file

@ -9,20 +9,15 @@ import {
import { InfoTooltip } from '@/components/InfoTooltip';
import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector';
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
interface GeneralTabProps {
configLoaded?: boolean;
}
export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
const {
model,
contextSize,
handleModelChange,
handleSelectModelFile,
handleContextSizeChangeWithStep,
} = useLaunchConfig();
const { model, contextSize, setModel, selectFile, setContextSizeWithStep } =
useLaunchConfigStore();
return (
<Transition
@ -40,8 +35,8 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
value={model}
placeholder="Select a .gguf model file or enter a direct URL to file"
tooltip="Select a GGUF text generation model file for chat and completion tasks."
onChange={handleModelChange}
onSelectFile={handleSelectModelFile}
onChange={setModel}
onSelectFile={() => selectFile('model', 'Select Text Model')}
searchUrl="https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending"
showAnalyze
paramType="model"
@ -58,9 +53,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
<TextInput
value={contextSize?.toString() || ''}
onChange={(event) =>
handleContextSizeChangeWithStep(
Number(event.target.value) || 256
)
setContextSizeWithStep(Number(event.target.value) || 256)
}
type="number"
min={256}
@ -75,7 +68,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
min={256}
max={131072}
step={1}
onChange={handleContextSizeChangeWithStep}
onChange={setContextSizeWithStep}
style={{ marginBottom: '0.5rem' }}
/>
</div>

View file

@ -4,7 +4,7 @@ import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
export const ImageGenerationTab = () => {
const {
@ -18,25 +18,19 @@ export const ImageGenerationTab = () => {
sdconvdirect,
sdvaecpu,
sdclipgpu,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
handleSdclipgChange,
handleSdphotomakerChange,
handleSdvaeChange,
handleSdloraChange,
handleSdconvdirectChange,
handleSdvaecpuChange,
handleSdclipgpuChange,
handleApplyPreset,
handleSelectSdmodelFile,
handleSelectSdt5xxlFile,
handleSelectSdcliplFile,
handleSelectSdclipgFile,
handleSelectSdphotomakerFile,
handleSelectSdvaeFile,
handleSelectSdloraFile,
} = useLaunchConfig();
setSdmodel,
setSdt5xxl,
setSdclipl,
setSdclipg,
setSdphotomaker,
setSdvae,
setSdlora,
setSdconvdirect,
setSdvaecpu,
setSdclipgpu,
applyPreset,
selectFile,
} = useLaunchConfigStore();
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
@ -54,15 +48,15 @@ export const ImageGenerationTab = () => {
onChange={(value) => {
setSelectedPreset(value);
if (value) {
handleApplyPreset(value);
applyPreset(value);
} else {
handleSdmodelChange('');
handleSdt5xxlChange('');
handleSdcliplChange('');
handleSdclipgChange('');
handleSdphotomakerChange('');
handleSdvaeChange('');
handleSdloraChange('');
setSdmodel('');
setSdt5xxl('');
setSdclipl('');
setSdclipg('');
setSdphotomaker('');
setSdvae('');
setSdlora('');
}
}}
clearable
@ -73,8 +67,8 @@ export const ImageGenerationTab = () => {
value={sdmodel}
placeholder="Select a model file or enter a direct URL"
tooltip="The primary image generation model. This is the main model that will generate images."
onChange={handleSdmodelChange}
onSelectFile={handleSelectSdmodelFile}
onChange={setSdmodel}
onSelectFile={() => selectFile('sdmodel', 'Select Image Model')}
searchUrl="https://huggingface.co/models?pipeline_tag=text-to-image&library=gguf&sort=trending"
showAnalyze
paramType="sdmodel"
@ -85,8 +79,8 @@ export const ImageGenerationTab = () => {
value={sdt5xxl}
placeholder="Select a T5-XXL encoder file or enter a direct URL"
tooltip="T5-XXL text encoder model for advanced text understanding."
onChange={handleSdt5xxlChange}
onSelectFile={handleSelectSdt5xxlFile}
onChange={setSdt5xxl}
onSelectFile={() => selectFile('sdt5xxl', 'Select T5XXL Model')}
searchUrl="https://huggingface.co/models?search=t5-xxl&library=gguf&sort=trending"
paramType="sdt5xxl"
/>
@ -96,8 +90,8 @@ export const ImageGenerationTab = () => {
value={sdclipl}
placeholder="Select a Clip-L file or enter a direct URL"
tooltip="CLIP-L text encoder model for text-image understanding."
onChange={handleSdcliplChange}
onSelectFile={handleSelectSdcliplFile}
onChange={setSdclipl}
onSelectFile={() => selectFile('sdclipl', 'Select CLIP-L Model')}
searchUrl="https://huggingface.co/models?search=clip-l&library=gguf&sort=trending"
paramType="sdclipl"
/>
@ -107,8 +101,8 @@ export const ImageGenerationTab = () => {
value={sdclipg}
placeholder="Select a Clip-G file or enter a direct URL"
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
onChange={handleSdclipgChange}
onSelectFile={handleSelectSdclipgFile}
onChange={setSdclipg}
onSelectFile={() => selectFile('sdclipg', 'Select CLIP-G Model')}
searchUrl="https://huggingface.co/models?search=clip-g&library=gguf&sort=trending"
paramType="sdclipg"
/>
@ -118,8 +112,10 @@ export const ImageGenerationTab = () => {
value={sdphotomaker}
placeholder="Select a PhotoMaker file or enter a direct URL"
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
onChange={handleSdphotomakerChange}
onSelectFile={handleSelectSdphotomakerFile}
onChange={setSdphotomaker}
onSelectFile={() =>
selectFile('sdphotomaker', 'Select PhotoMaker Model')
}
searchUrl="https://huggingface.co/models?search=photomaker&library=safetensors&sort=trending"
paramType="sdphotomaker"
/>
@ -129,8 +125,8 @@ export const ImageGenerationTab = () => {
value={sdvae}
placeholder="Select a VAE file or enter a direct URL"
tooltip="Variational Autoencoder model for improved image quality."
onChange={handleSdvaeChange}
onSelectFile={handleSelectSdvaeFile}
onChange={setSdvae}
onSelectFile={() => selectFile('sdvae', 'Select VAE Model')}
searchUrl="https://huggingface.co/models?search=vae&library=safetensors&sort=trending"
paramType="sdvae"
/>
@ -140,8 +136,8 @@ export const ImageGenerationTab = () => {
value={sdlora}
placeholder="Select a LoRa file or enter a direct URL"
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
onChange={handleSdloraChange}
onSelectFile={handleSelectSdloraFile}
onChange={setSdlora}
onSelectFile={() => selectFile('sdlora', 'Select LoRA Model')}
searchUrl="https://huggingface.co/models?search=lora&library=safetensors&sort=trending"
paramType="sdlora"
/>
@ -152,7 +148,7 @@ export const ImageGenerationTab = () => {
value={sdconvdirect}
onChange={(value) => {
if (value === 'off' || value === 'vaeonly' || value === 'full') {
handleSdconvdirectChange(value);
setSdconvdirect(value);
}
}}
data={[
@ -167,14 +163,14 @@ export const ImageGenerationTab = () => {
label="Force VAE to CPU"
tooltip="Forces the VAE (Variational Autoencoder) to run on CPU instead of GPU. This can save VRAM but will be slower. Useful for systems with limited GPU memory."
checked={sdvaecpu}
onChange={handleSdvaecpuChange}
onChange={setSdvaecpu}
/>
<CheckboxWithTooltip
label="Offload CLIP/T5"
tooltip="Offloads CLIP and T5 text encoders to the GPU for faster processing. By default they run on CPU. Only enable if you have VRAM to spare."
checked={sdclipgpu}
onChange={handleSdclipgpuChange}
onChange={setSdclipgpu}
/>
</Group>
</Stack>

View file

@ -2,7 +2,7 @@ import { Stack, Text, TextInput, Group } from '@mantine/core';
import { useState } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
export const NetworkTab = () => {
const {
@ -13,14 +13,14 @@ export const NetworkTab = () => {
remotetunnel,
nocertify,
websearch,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
} = useLaunchConfig();
setPort,
setHost,
setMultiuser,
setMultiplayer,
setRemotetunnel,
setNocertify,
setWebsearch,
} = useLaunchConfigStore();
const [portInput, setPortInput] = useState('');
return (
@ -36,7 +36,7 @@ export const NetworkTab = () => {
<TextInput
placeholder="localhost"
value={host}
onChange={(event) => handleHostChange(event.currentTarget.value)}
onChange={(event) => setHost(event.currentTarget.value)}
/>
</div>
@ -60,13 +60,13 @@ export const NetworkTab = () => {
const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
handlePortChange(numValue);
setPort(numValue);
}
}}
onBlur={(event) => {
const value = event.currentTarget.value;
if (value === '') {
handlePortChange(undefined);
setPort(undefined);
setPortInput('');
}
}}
@ -83,14 +83,14 @@ export const NetworkTab = () => {
<Group gap="lg" align="flex-start" wrap="nowrap">
<CheckboxWithTooltip
checked={multiuser}
onChange={handleMultiuserChange}
onChange={setMultiuser}
label="Multiuser Mode"
tooltip="Allows requests by multiple different clients to be queued and handled in sequence."
/>
<CheckboxWithTooltip
checked={multiplayer}
onChange={handleMultiplayerChange}
onChange={setMultiplayer}
label="Shared Multiplayer"
tooltip="Hosts a shared multiplayer session"
/>
@ -99,14 +99,14 @@ export const NetworkTab = () => {
<Group gap="lg" align="flex-start" wrap="nowrap">
<CheckboxWithTooltip
checked={remotetunnel}
onChange={handleRemotetunnelChange}
onChange={setRemotetunnel}
label="Remote Tunnel"
tooltip="Creates a trycloudflare tunnel. Allows you to access your server from other devices over an internet URL."
/>
<CheckboxWithTooltip
checked={nocertify}
onChange={handleNocertifyChange}
onChange={setNocertify}
label="No Certify Mode (Insecure)"
tooltip="Allows insecure SSL connections. Use this if you have SSL cert errors and need to bypass certificate restrictions."
/>
@ -114,7 +114,7 @@ export const NetworkTab = () => {
<CheckboxWithTooltip
checked={websearch}
onChange={handleWebsearchChange}
onChange={setWebsearch}
label="Enable WebSearch"
tooltip="Enable the local search engine proxy so Web Searches can be done."
/>

View file

@ -1,7 +1,7 @@
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
import { useState, useEffect, useCallback, useRef } from 'react';
import { logError } from '@/utils/logger';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
@ -65,9 +65,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
moeexperts,
parseAndApplyConfigFile,
loadConfigFromFile,
handleModelChange,
handleBackendChange,
} = useLaunchConfig();
setModel,
setBackend,
} = useLaunchConfigStore();
const { isLaunching, handleLaunch } = useLaunchLogic({
model,
@ -87,9 +87,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
await window.electronAPI.kobold.getAvailableAccelerations();
if (!backend && accelerations && accelerations.length > 0) {
handleBackendChange(accelerations[0].value);
setBackend(accelerations[0].value);
}
}, [backend, handleBackendChange]);
}, [backend, setBackend]);
const setInitialDefaults = useCallback(
(currentModel: string, currentSdModel: string) => {
@ -98,11 +98,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
!currentModel.trim() &&
!currentSdModel.trim()
) {
handleModelChange(DEFAULT_MODEL_URL);
setModel(DEFAULT_MODEL_URL);
defaultsSetRef.current = true;
}
},
[handleModelChange]
[setModel]
);
useEffect(() => {

View file

@ -9,7 +9,7 @@ export const STATUSBAR_HEIGHT = '1.5rem';
export const SERVER_READY_SIGNALS = {
KOBOLDCPP: 'Please connect to custom endpoint at',
SILLYTAVERN: 'SillyTavern is listening on',
OPENWEBUI: 'Waiting for application startup.',
OPENWEBUI: 'Started server process',
} as const;
export const DEFAULT_CONTEXT_SIZE = 4096;

View file

@ -1,32 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
import type { FrontendPreference } from '@/types';
export function useFrontendPreference() {
const [frontendPreference, setFrontendPreferenceState] =
useState<FrontendPreference>('koboldcpp');
useEffect(() => {
const loadFromConfig = async () => {
const preference = (await window.electronAPI.config.get(
'frontendPreference'
)) as FrontendPreference;
setFrontendPreferenceState(preference || 'koboldcpp');
};
loadFromConfig();
}, []);
const setFrontendPreference = useCallback(
(preference: FrontendPreference) => {
setFrontendPreferenceState(preference);
window.electronAPI.config.set('frontendPreference', preference);
},
[]
);
return {
frontendPreference,
setFrontendPreference,
};
}

View file

@ -1,108 +0,0 @@
import { useLaunchConfigStore } from '@/stores/launchConfig';
import {
type ImageModelPreset,
IMAGE_MODEL_PRESETS,
} from '@/constants/imageModelPresets';
export const useLaunchConfig = () => {
const state = useLaunchConfigStore();
return {
gpuLayers: state.gpuLayers,
autoGpuLayers: state.autoGpuLayers,
contextSize: state.contextSize,
model: state.model,
additionalArguments: state.additionalArguments,
preLaunchCommands: state.preLaunchCommands,
port: state.port,
host: state.host,
multiuser: state.multiuser,
multiplayer: state.multiplayer,
remotetunnel: state.remotetunnel,
nocertify: state.nocertify,
websearch: state.websearch,
noshift: state.noshift,
flashattention: state.flashattention,
noavx2: state.noavx2,
failsafe: state.failsafe,
lowvram: state.lowvram,
quantmatmul: state.quantmatmul,
usemmap: state.usemmap,
debugmode: state.debugmode,
backend: state.backend,
gpuDeviceSelection: state.gpuDeviceSelection,
tensorSplit: state.tensorSplit,
gpuPlatform: state.gpuPlatform,
sdmodel: state.sdmodel,
sdt5xxl: state.sdt5xxl,
sdclipl: state.sdclipl,
sdclipg: state.sdclipg,
sdphotomaker: state.sdphotomaker,
sdvae: state.sdvae,
sdlora: state.sdlora,
sdconvdirect: state.sdconvdirect,
sdvaecpu: state.sdvaecpu,
sdclipgpu: state.sdclipgpu,
moecpu: state.moecpu,
moeexperts: state.moeexperts,
handleGpuLayersChange: state.setGpuLayers,
handleAutoGpuLayersChange: state.setAutoGpuLayers,
handleContextSizeChangeWithStep: state.contextSizeChangeWithStep,
handleModelChange: state.setModel,
handleAdditionalArgumentsChange: state.setAdditionalArguments,
handlePortChange: state.setPort,
handleHostChange: state.setHost,
handleMultiuserChange: state.setMultiuser,
handleMultiplayerChange: state.setMultiplayer,
handleRemotetunnelChange: state.setRemotetunnel,
handleNocertifyChange: state.setNocertify,
handleWebsearchChange: state.setWebsearch,
handleNoshiftChange: state.setNoshift,
handleFlashattentionChange: state.setFlashattention,
handleNoavx2Change: state.setNoavx2,
handleFailsafeChange: state.setFailsafe,
handleLowvramChange: state.setLowvram,
handleQuantmatmulChange: state.setQuantmatmul,
handleUsemmapChange: state.setUsemmap,
handleDebugmodeChange: state.setDebugmode,
handlePreLaunchCommandsChange: state.setPreLaunchCommands,
handleBackendChange: state.setBackend,
handleGpuDeviceSelectionChange: state.setGpuDeviceSelection,
handleTensorSplitChange: state.setTensorSplit,
handleGpuPlatformChange: state.setGpuPlatform,
handleSdmodelChange: state.setSdmodel,
handleSdt5xxlChange: state.setSdt5xxl,
handleSdcliplChange: state.setSdclipl,
handleSdclipgChange: state.setSdclipg,
handleSdphotomakerChange: state.setSdphotomaker,
handleSdvaeChange: state.setSdvae,
handleSdloraChange: state.setSdlora,
handleSdconvdirectChange: state.setSdconvdirect,
handleSdvaecpuChange: state.setSdvaecpu,
handleSdclipgpuChange: state.setSdclipgpu,
handleMoecpuChange: state.setMoecpu,
handleMoeexpertsChange: state.setMoeexperts,
parseAndApplyConfigFile: state.parseAndApplyConfigFile,
loadConfigFromFile: state.loadConfigFromFile,
handleSelectModelFile: state.selectModelFile,
handleImageModelPresetChange: state.applyImageModelPreset,
handleApplyPreset: (presetName: string) => {
const preset = IMAGE_MODEL_PRESETS.find(
(p: ImageModelPreset) => p.name === presetName
);
if (preset) {
state.applyImageModelPreset(preset);
}
},
handleSelectSdmodelFile: state.selectSdmodelFile,
handleSelectSdt5xxlFile: state.selectSdt5xxlFile,
handleSelectSdcliplFile: state.selectSdcliplFile,
handleSelectSdclipgFile: state.selectSdclipgFile,
handleSelectSdphotomakerFile: state.selectSdphotomakerFile,
handleSelectSdvaeFile: state.selectSdvaeFile,
handleSelectSdloraFile: state.selectSdloraFile,
};
};

View file

@ -213,7 +213,7 @@ export async function launchKoboldCpp(
sendKoboldOutput(commandLine);
if (remotetunnel) {
startTunnel();
startTunnel(frontendPreference);
}
let readyResolve:
@ -232,6 +232,13 @@ export async function launchKoboldCpp(
});
const handleServerReady = () => {
const isKoboldFrontend =
frontendPreference === 'koboldcpp' ||
(!isTextMode && imageGenerationFrontendPreference === 'builtin');
if (isKoboldFrontend) {
sendToRenderer('server-ready');
}
readyResolve?.({ success: true, pid: child.pid });
};

View file

@ -6,7 +6,7 @@ import { get as httpsGet } from 'https';
import { getInstallDir } from '@/main/modules/config';
import { pathExists } from '@/utils/node/fs';
import { logError } from '@/utils/node/logging';
import { sendToRenderer } from '@/main/modules/window';
import { sendKoboldOutput } from '@/main/modules/window';
import type { ModelParamType, CachedModel } from '@/types';
import type { IncomingMessage } from 'http';
@ -176,7 +176,7 @@ async function downloadFile(
? `Downloaded ${downloadedMB}MB / ${totalMB}MB (${percent}%) - ${speedMBPerSec}MB/s - ETA: ${etaStr}`
: `Downloaded ${downloadedMB}MB - ${speedMBPerSec}MB/s`;
sendToRenderer('kobold-output', `\r${progressMsg}`);
sendKoboldOutput(`\r${progressMsg}`);
onProgress({
type: 'progress',
@ -210,7 +210,7 @@ async function downloadFile(
fileStream.on('finish', async () => {
try {
await rename(tempPath, outputPath);
sendToRenderer('kobold-output', '\n');
sendKoboldOutput('\n');
activeDownloads.delete(abortController);
resolve(true);
} catch (err) {
@ -270,7 +270,7 @@ export async function resolveModelPath(
const localPath = getModelLocalPath(urlOrPath, paramType);
if (await pathExists(localPath)) {
sendToRenderer('kobold-output', `Using cached model at: ${localPath}\n`);
sendKoboldOutput(`Using cached model at: ${localPath}\n`);
onProgress?.({
type: 'complete',
localPath,
@ -278,20 +278,14 @@ export async function resolveModelPath(
return localPath;
}
sendToRenderer(
'kobold-output',
`Downloading model from ${urlOrPath} to ${localPath}...\n`
);
sendKoboldOutput(`Downloading model from ${urlOrPath} to ${localPath}...\n`);
const progressCallback = onProgress || ((p: DownloadProgress) => p);
try {
await downloadFile(urlOrPath, localPath, progressCallback);
sendToRenderer(
'kobold-output',
`Model downloaded successfully to: ${localPath}\n\n`
);
sendKoboldOutput(`Model downloaded successfully to: ${localPath}\n\n`);
progressCallback({
type: 'complete',
localPath,

View file

@ -4,11 +4,26 @@ import { Tunnel, bin, install } from 'cloudflared';
import { logError } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from '../window';
import { PROXY } from '@/constants/proxy';
import { SILLYTAVERN, OPENWEBUI } from '@/constants';
import type { FrontendPreference } from '@/types';
let activeTunnel: Tunnel | null = null;
let tunnelUrl: string | null = null;
export const startTunnel = async () => {
const getTunnelTarget = (frontendPreference: FrontendPreference) => {
switch (frontendPreference) {
case 'sillytavern':
return `http://${SILLYTAVERN.HOST}:${SILLYTAVERN.PROXY_PORT}`;
case 'openwebui':
return `http://${OPENWEBUI.HOST}:${OPENWEBUI.PORT}`;
default:
return `http://${PROXY.HOST}:${PROXY.PORT}`;
}
};
export const startTunnel = async (
frontendPreference: FrontendPreference = 'koboldcpp'
) => {
if (activeTunnel) {
return tunnelUrl;
}
@ -22,7 +37,8 @@ export const startTunnel = async () => {
sendKoboldOutput('cloudflared binary installed');
}
const tunnel = Tunnel.quick(`http://${PROXY.HOST}:${PROXY.PORT}`, {
const tunnelTarget = getTunnelTarget(frontendPreference);
const tunnel = Tunnel.quick(tunnelTarget, {
'--no-autoupdate': true,
});

View file

@ -5,7 +5,7 @@ import { app } from 'electron';
import type { ChildProcess } from 'child_process';
import { logError } from '@/utils/node/logging';
import { sendKoboldOutput } from './window';
import { sendKoboldOutput, sendToRenderer } from './window';
import { getInstallDir } from './config';
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
import { terminateProcess } from '@/utils/node/process';
@ -38,22 +38,28 @@ async function createUvProcess(args: string[], env?: Record<string, string>) {
async function waitForOpenWebUIToStart() {
return new Promise<void>((resolve, reject) => {
let resolved = false;
const checkForOutput = (data: Buffer) => {
if (resolved) return;
const output = data.toString();
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
resolved = true;
sendKoboldOutput('Open WebUI is now running!');
sendToRenderer('server-ready');
resolve();
if (openWebUIProcess?.stdout) {
openWebUIProcess.stdout.removeListener('data', checkForOutput);
}
}
};
if (openWebUIProcess?.stdout) {
openWebUIProcess.stdout.on('data', checkForOutput);
} else {
reject(new Error('Open WebUI process stdout not available'));
}
if (openWebUIProcess?.stderr) {
openWebUIProcess.stderr.on('data', checkForOutput);
}
if (!openWebUIProcess?.stdout && !openWebUIProcess?.stderr) {
reject(new Error('Open WebUI process streams not available'));
}
});
}

View file

@ -6,7 +6,7 @@ import { platform, on } from 'process';
import type { ChildProcess } from 'child_process';
import { logError, tryExecute } from '@/utils/node/logging';
import { sendKoboldOutput } from './window';
import { sendKoboldOutput, sendToRenderer } from './window';
import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
import { PROXY } from '@/constants/proxy';
import { terminateProcess } from '@/utils/node/process';
@ -223,6 +223,7 @@ async function waitForSillyTavernToStart() {
const checkForOutput = (data: Buffer) => {
if (data.toString().includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
sendKoboldOutput('SillyTavern is now running!');
sendToRenderer('server-ready');
resolve();
if (sillyTavernProcess?.stdout) {

View file

@ -115,6 +115,14 @@ const koboldAPI: KoboldAPI = {
ipcRenderer.removeListener('kobold-crashed', handler);
};
},
onServerReady: (callback) => {
const handler = () => callback();
ipcRenderer.on('server-ready', handler);
return () => {
ipcRenderer.removeListener('server-ready', handler);
};
},
onTunnelUrlChanged: (callback) => {
const handler = (_: IpcRendererEvent, url: string | null) => callback(url);
ipcRenderer.on('tunnel-url-changed', handler);

View file

@ -1,6 +1,6 @@
import { create } from 'zustand';
import type { ConfigFile, SdConvDirectMode } from '@/types';
import type { ImageModelPreset } from '@/constants/imageModelPresets';
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
import { DEFAULT_AUTO_GPU_LAYERS, DEFAULT_CONTEXT_SIZE } from '@/constants';
interface LaunchConfigState {
@ -87,16 +87,20 @@ interface LaunchConfigState {
configFiles: ConfigFile[],
savedConfig: string | null
) => Promise<string | null>;
selectModelFile: () => Promise<void>;
selectSdmodelFile: () => Promise<void>;
selectSdt5xxlFile: () => Promise<void>;
selectSdcliplFile: () => Promise<void>;
selectSdclipgFile: () => Promise<void>;
selectSdphotomakerFile: () => Promise<void>;
selectSdvaeFile: () => Promise<void>;
selectSdloraFile: () => Promise<void>;
contextSizeChangeWithStep: (size: number) => void;
applyImageModelPreset: (preset: ImageModelPreset) => void;
selectFile: (
field:
| 'model'
| 'sdmodel'
| 'sdt5xxl'
| 'sdclipl'
| 'sdclipg'
| 'sdphotomaker'
| 'sdvae'
| 'sdlora',
title: string
) => Promise<void>;
setContextSizeWithStep: (size: number) => void;
applyPreset: (presetName: string) => void;
}
export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
@ -445,90 +449,23 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
return null;
},
selectModelFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a Text Model File'
);
if (result) {
set({
model: result,
isTextMode: Boolean(result?.trim()),
});
}
selectFile: async (field, title) => {
const result = await window.electronAPI.kobold.selectModelFile(title);
if (!result) return;
if (field === 'model') set({ model: result, isTextMode: true });
else if (field === 'sdmodel')
set({ sdmodel: result, isImageGenerationMode: true });
else set({ [field]: result });
},
selectSdmodelFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a Image Gen. Model File'
);
if (result) {
set({
sdmodel: result,
isImageGenerationMode: Boolean(result?.trim()),
});
}
setContextSizeWithStep: (size: number) => {
const rounded = Math.round(size / 256) * 256;
set({ contextSize: Math.max(256, Math.min(131072, rounded)) });
},
selectSdt5xxlFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a T5XXL Model File'
);
if (result) {
set({ sdt5xxl: result });
}
},
selectSdcliplFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a CLIP-L Model File'
);
if (result) {
set({ sdclipl: result });
}
},
selectSdclipgFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a CLIP-G Model File'
);
if (result) {
set({ sdclipg: result });
}
},
selectSdphotomakerFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a PhotoMaker Model File'
);
if (result) {
set({ sdphotomaker: result });
}
},
selectSdvaeFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a VAE Model File'
);
if (result) {
set({ sdvae: result });
}
},
selectSdloraFile: async () => {
const result = await window.electronAPI.kobold.selectModelFile(
'Select a LORA Model File'
);
if (result) {
set({ sdlora: result });
}
},
contextSizeChangeWithStep: (size: number) => {
const roundedSize = Math.round(size / 256) * 256;
set({ contextSize: Math.max(256, Math.min(131072, roundedSize)) });
},
applyImageModelPreset: (preset: ImageModelPreset) => {
applyPreset: (presetName: string) => {
const preset = IMAGE_MODEL_PRESETS.find((p) => p.name === presetName);
if (preset) {
set({
sdmodel: preset.sdmodel,
isImageGenerationMode: Boolean(preset.sdmodel?.trim()),
@ -538,5 +475,6 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
sdvae: preset.sdvae,
model: '',
});
}
},
}));

View file

@ -180,6 +180,7 @@ export interface KoboldAPI {
onKoboldCrashed: (
callback: (crashInfo: KoboldCrashInfo) => void
) => () => void;
onServerReady: (callback: () => void) => () => void;
onTunnelUrlChanged: (callback: (url: string | null) => void) => () => void;
}

2
src/types/ipc.d.ts vendored
View file

@ -4,6 +4,7 @@ export type IPCChannel =
| 'versions-updated'
| 'kobold-output'
| 'kobold-crashed'
| 'server-ready'
| 'tunnel-url-changed'
| 'window-maximized'
| 'window-unmaximized'
@ -21,6 +22,7 @@ export interface IPCChannelPayloads {
'versions-updated': [];
'kobold-output': [message: string];
'kobold-crashed': [crashInfo: KoboldCrashInfo];
'server-ready': [];
'tunnel-url-changed': [url: string | null];
'window-maximized': [];
'window-unmaximized': [];