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

View file

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

View file

@ -7,31 +7,16 @@ import {
} from 'react'; } from 'react';
import { Box, ScrollArea, ActionIcon } from '@mantine/core'; import { Box, ScrollArea, ActionIcon } from '@mantine/core';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
SERVER_READY_SIGNALS,
STATUSBAR_HEIGHT,
TITLEBAR_HEIGHT,
} from '@/constants';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal'; import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
interface TerminalTabProps {
onServerReady: () => void;
}
export interface TerminalTabRef { export interface TerminalTabRef {
scrollToBottom: () => void; scrollToBottom: () => void;
} }
export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>( export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
({ onServerReady }, ref) => { const { resolvedColorScheme: colorScheme } = usePreferencesStore();
const { isImageGenerationMode, isTextMode } = useLaunchConfigStore();
const {
frontendPreference,
imageGenerationFrontendPreference,
resolvedColorScheme: colorScheme,
} = usePreferencesStore();
const [terminalContent, setTerminalContent] = useState(''); const [terminalContent, setTerminalContent] = useState('');
const [isUserScrolling, setIsUserScrolling] = useState(false); const [isUserScrolling, setIsUserScrolling] = useState(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
@ -73,44 +58,12 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
}, [terminalContent, shouldAutoScroll, isUserScrolling]); }, [terminalContent, shouldAutoScroll, isUserScrolling]);
useEffect(() => { useEffect(() => {
const cleanup = window.electronAPI.kobold.onKoboldOutput( const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
(data: string) => { setTerminalContent((prev) => handleTerminalOutput(prev, data.toString()));
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);
}); });
}
);
return cleanup; return cleanup;
}, [ }, []);
onServerReady,
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode,
isTextMode,
]);
const scrollToBottom = () => { const scrollToBottom = () => {
if (viewportRef.current) { if (viewportRef.current) {
@ -197,7 +150,6 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
)} )}
</Box> </Box>
); );
} });
);
TerminalTab.displayName = 'TerminalTab'; 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 { ServerTab } from '@/components/screens/Interface/ServerTab';
import { import {
TerminalTab, TerminalTab,
type TerminalTabRef, type TerminalTabRef,
} from '@/components/screens/Interface/TerminalTab'; } 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'; import type { InterfaceTab } from '@/types';
interface InterfaceScreenProps { interface InterfaceScreenProps {
activeTab?: InterfaceTab | null; activeTab?: InterfaceTab | null;
onTabChange?: (tab: InterfaceTab) => void; isServerReady: boolean;
} }
export const InterfaceScreen = ({ export const InterfaceScreen = ({
activeTab, activeTab,
onTabChange, isServerReady,
}: InterfaceScreenProps) => { }: InterfaceScreenProps) => {
const [isServerReady, setIsServerReady] = useState(false);
const terminalTabRef = useRef<TerminalTabRef>(null); 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(() => { useEffect(() => {
if (activeTab === 'terminal' && terminalTabRef.current) { if (activeTab === 'terminal' && terminalTabRef.current) {
terminalTabRef.current.scrollToBottom(); terminalTabRef.current.scrollToBottom();
@ -84,7 +52,7 @@ export const InterfaceScreen = ({
display: activeTab === 'terminal' ? 'block' : 'none', display: activeTab === 'terminal' ? 'block' : 'none',
}} }}
> >
<TerminalTab ref={terminalTabRef} onServerReady={handleServerReady} /> <TerminalTab ref={terminalTabRef} />
</div> </div>
</div> </div>
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { Stack, Text, TextInput, Group } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
export const NetworkTab = () => { export const NetworkTab = () => {
const { const {
@ -13,14 +13,14 @@ export const NetworkTab = () => {
remotetunnel, remotetunnel,
nocertify, nocertify,
websearch, websearch,
handlePortChange, setPort,
handleHostChange, setHost,
handleMultiuserChange, setMultiuser,
handleMultiplayerChange, setMultiplayer,
handleRemotetunnelChange, setRemotetunnel,
handleNocertifyChange, setNocertify,
handleWebsearchChange, setWebsearch,
} = useLaunchConfig(); } = useLaunchConfigStore();
const [portInput, setPortInput] = useState(''); const [portInput, setPortInput] = useState('');
return ( return (
@ -36,7 +36,7 @@ export const NetworkTab = () => {
<TextInput <TextInput
placeholder="localhost" placeholder="localhost"
value={host} value={host}
onChange={(event) => handleHostChange(event.currentTarget.value)} onChange={(event) => setHost(event.currentTarget.value)}
/> />
</div> </div>
@ -60,13 +60,13 @@ export const NetworkTab = () => {
const numValue = Number(value); const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) { if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
handlePortChange(numValue); setPort(numValue);
} }
}} }}
onBlur={(event) => { onBlur={(event) => {
const value = event.currentTarget.value; const value = event.currentTarget.value;
if (value === '') { if (value === '') {
handlePortChange(undefined); setPort(undefined);
setPortInput(''); setPortInput('');
} }
}} }}
@ -83,14 +83,14 @@ export const NetworkTab = () => {
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<CheckboxWithTooltip <CheckboxWithTooltip
checked={multiuser} checked={multiuser}
onChange={handleMultiuserChange} onChange={setMultiuser}
label="Multiuser Mode" label="Multiuser Mode"
tooltip="Allows requests by multiple different clients to be queued and handled in sequence." tooltip="Allows requests by multiple different clients to be queued and handled in sequence."
/> />
<CheckboxWithTooltip <CheckboxWithTooltip
checked={multiplayer} checked={multiplayer}
onChange={handleMultiplayerChange} onChange={setMultiplayer}
label="Shared Multiplayer" label="Shared Multiplayer"
tooltip="Hosts a shared multiplayer session" tooltip="Hosts a shared multiplayer session"
/> />
@ -99,14 +99,14 @@ export const NetworkTab = () => {
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<CheckboxWithTooltip <CheckboxWithTooltip
checked={remotetunnel} checked={remotetunnel}
onChange={handleRemotetunnelChange} onChange={setRemotetunnel}
label="Remote Tunnel" label="Remote Tunnel"
tooltip="Creates a trycloudflare tunnel. Allows you to access your server from other devices over an internet URL." tooltip="Creates a trycloudflare tunnel. Allows you to access your server from other devices over an internet URL."
/> />
<CheckboxWithTooltip <CheckboxWithTooltip
checked={nocertify} checked={nocertify}
onChange={handleNocertifyChange} onChange={setNocertify}
label="No Certify Mode (Insecure)" label="No Certify Mode (Insecure)"
tooltip="Allows insecure SSL connections. Use this if you have SSL cert errors and need to bypass certificate restrictions." 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 <CheckboxWithTooltip
checked={websearch} checked={websearch}
onChange={handleWebsearchChange} onChange={setWebsearch}
label="Enable WebSearch" label="Enable WebSearch"
tooltip="Enable the local search engine proxy so Web Searches can be done." 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 { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { logError } from '@/utils/logger'; import { logError } from '@/utils/logger';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { useLaunchLogic } from '@/hooks/useLaunchLogic'; import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings'; import { useWarnings } from '@/hooks/useWarnings';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index'; import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
@ -65,9 +65,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
moeexperts, moeexperts,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadConfigFromFile, loadConfigFromFile,
handleModelChange, setModel,
handleBackendChange, setBackend,
} = useLaunchConfig(); } = useLaunchConfigStore();
const { isLaunching, handleLaunch } = useLaunchLogic({ const { isLaunching, handleLaunch } = useLaunchLogic({
model, model,
@ -87,9 +87,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
await window.electronAPI.kobold.getAvailableAccelerations(); await window.electronAPI.kobold.getAvailableAccelerations();
if (!backend && accelerations && accelerations.length > 0) { if (!backend && accelerations && accelerations.length > 0) {
handleBackendChange(accelerations[0].value); setBackend(accelerations[0].value);
} }
}, [backend, handleBackendChange]); }, [backend, setBackend]);
const setInitialDefaults = useCallback( const setInitialDefaults = useCallback(
(currentModel: string, currentSdModel: string) => { (currentModel: string, currentSdModel: string) => {
@ -98,11 +98,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
!currentModel.trim() && !currentModel.trim() &&
!currentSdModel.trim() !currentSdModel.trim()
) { ) {
handleModelChange(DEFAULT_MODEL_URL); setModel(DEFAULT_MODEL_URL);
defaultsSetRef.current = true; defaultsSetRef.current = true;
} }
}, },
[handleModelChange] [setModel]
); );
useEffect(() => { useEffect(() => {

View file

@ -9,7 +9,7 @@ export const STATUSBAR_HEIGHT = '1.5rem';
export const SERVER_READY_SIGNALS = { export const SERVER_READY_SIGNALS = {
KOBOLDCPP: 'Please connect to custom endpoint at', KOBOLDCPP: 'Please connect to custom endpoint at',
SILLYTAVERN: 'SillyTavern is listening on', SILLYTAVERN: 'SillyTavern is listening on',
OPENWEBUI: 'Waiting for application startup.', OPENWEBUI: 'Started server process',
} as const; } as const;
export const DEFAULT_CONTEXT_SIZE = 4096; 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); sendKoboldOutput(commandLine);
if (remotetunnel) { if (remotetunnel) {
startTunnel(); startTunnel(frontendPreference);
} }
let readyResolve: let readyResolve:
@ -232,6 +232,13 @@ export async function launchKoboldCpp(
}); });
const handleServerReady = () => { const handleServerReady = () => {
const isKoboldFrontend =
frontendPreference === 'koboldcpp' ||
(!isTextMode && imageGenerationFrontendPreference === 'builtin');
if (isKoboldFrontend) {
sendToRenderer('server-ready');
}
readyResolve?.({ success: true, pid: child.pid }); 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 { getInstallDir } from '@/main/modules/config';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { logError } from '@/utils/node/logging'; 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 { ModelParamType, CachedModel } from '@/types';
import type { IncomingMessage } from 'http'; 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 / ${totalMB}MB (${percent}%) - ${speedMBPerSec}MB/s - ETA: ${etaStr}`
: `Downloaded ${downloadedMB}MB - ${speedMBPerSec}MB/s`; : `Downloaded ${downloadedMB}MB - ${speedMBPerSec}MB/s`;
sendToRenderer('kobold-output', `\r${progressMsg}`); sendKoboldOutput(`\r${progressMsg}`);
onProgress({ onProgress({
type: 'progress', type: 'progress',
@ -210,7 +210,7 @@ async function downloadFile(
fileStream.on('finish', async () => { fileStream.on('finish', async () => {
try { try {
await rename(tempPath, outputPath); await rename(tempPath, outputPath);
sendToRenderer('kobold-output', '\n'); sendKoboldOutput('\n');
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
resolve(true); resolve(true);
} catch (err) { } catch (err) {
@ -270,7 +270,7 @@ export async function resolveModelPath(
const localPath = getModelLocalPath(urlOrPath, paramType); const localPath = getModelLocalPath(urlOrPath, paramType);
if (await pathExists(localPath)) { if (await pathExists(localPath)) {
sendToRenderer('kobold-output', `Using cached model at: ${localPath}\n`); sendKoboldOutput(`Using cached model at: ${localPath}\n`);
onProgress?.({ onProgress?.({
type: 'complete', type: 'complete',
localPath, localPath,
@ -278,20 +278,14 @@ export async function resolveModelPath(
return localPath; return localPath;
} }
sendToRenderer( sendKoboldOutput(`Downloading model from ${urlOrPath} to ${localPath}...\n`);
'kobold-output',
`Downloading model from ${urlOrPath} to ${localPath}...\n`
);
const progressCallback = onProgress || ((p: DownloadProgress) => p); const progressCallback = onProgress || ((p: DownloadProgress) => p);
try { try {
await downloadFile(urlOrPath, localPath, progressCallback); await downloadFile(urlOrPath, localPath, progressCallback);
sendToRenderer( sendKoboldOutput(`Model downloaded successfully to: ${localPath}\n\n`);
'kobold-output',
`Model downloaded successfully to: ${localPath}\n\n`
);
progressCallback({ progressCallback({
type: 'complete', type: 'complete',
localPath, localPath,

View file

@ -4,11 +4,26 @@ import { Tunnel, bin, install } from 'cloudflared';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from '../window'; import { sendKoboldOutput, sendToRenderer } from '../window';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { SILLYTAVERN, OPENWEBUI } from '@/constants';
import type { FrontendPreference } from '@/types';
let activeTunnel: Tunnel | null = null; let activeTunnel: Tunnel | null = null;
let tunnelUrl: string | 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) { if (activeTunnel) {
return tunnelUrl; return tunnelUrl;
} }
@ -22,7 +37,8 @@ export const startTunnel = async () => {
sendKoboldOutput('cloudflared binary installed'); 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, '--no-autoupdate': true,
}); });

View file

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

View file

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

View file

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

View file

@ -180,6 +180,7 @@ export interface KoboldAPI {
onKoboldCrashed: ( onKoboldCrashed: (
callback: (crashInfo: KoboldCrashInfo) => void callback: (crashInfo: KoboldCrashInfo) => void
) => () => void; ) => () => void;
onServerReady: (callback: () => void) => () => void;
onTunnelUrlChanged: (callback: (url: string | null) => 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' | 'versions-updated'
| 'kobold-output' | 'kobold-output'
| 'kobold-crashed' | 'kobold-crashed'
| 'server-ready'
| 'tunnel-url-changed' | 'tunnel-url-changed'
| 'window-maximized' | 'window-maximized'
| 'window-unmaximized' | 'window-unmaximized'
@ -21,6 +22,7 @@ export interface IPCChannelPayloads {
'versions-updated': []; 'versions-updated': [];
'kobold-output': [message: string]; 'kobold-output': [message: string];
'kobold-crashed': [crashInfo: KoboldCrashInfo]; 'kobold-crashed': [crashInfo: KoboldCrashInfo];
'server-ready': [];
'tunnel-url-changed': [url: string | null]; 'tunnel-url-changed': [url: string | null];
'window-maximized': []; 'window-maximized': [];
'window-unmaximized': []; 'window-unmaximized': [];