diff --git a/src/components/App/Router.tsx b/src/components/App/Router.tsx index e6560b6..9a62f9a 100644 --- a/src/components/App/Router.tsx +++ b/src/components/App/Router.tsx @@ -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 = ({ > diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index ae4c79a..a3f6f98 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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('terminal'); + const [isServerReady, setIsServerReady] = useState(false); const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false); const [crashInfo, setCrashInfo] = useState(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} /> )} diff --git a/src/components/screens/Interface/TerminalTab.tsx b/src/components/screens/Interface/TerminalTab.tsx index 91ced40..8408366 100644 --- a/src/components/screens/Interface/TerminalTab.tsx +++ b/src/components/screens/Interface/TerminalTab.tsx @@ -7,197 +7,149 @@ 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( - ({ onServerReady }, ref) => { - const { isImageGenerationMode, isTextMode } = useLaunchConfigStore(); - const { - frontendPreference, - imageGenerationFrontendPreference, - resolvedColorScheme: colorScheme, - } = usePreferencesStore(); - const [terminalContent, setTerminalContent] = useState(''); - const [isUserScrolling, setIsUserScrolling] = useState(false); - const [shouldAutoScroll, setShouldAutoScroll] = useState(true); - const [isVisible, setIsVisible] = useState(false); - const scrollAreaRef = useRef(null); - const viewportRef = useRef(null); - const lastScrollTop = useRef(0); +export const TerminalTab = forwardRef((_props, ref) => { + const { resolvedColorScheme: colorScheme } = usePreferencesStore(); + const [terminalContent, setTerminalContent] = useState(''); + const [isUserScrolling, setIsUserScrolling] = useState(false); + const [shouldAutoScroll, setShouldAutoScroll] = useState(true); + const [isVisible, setIsVisible] = useState(false); + const scrollAreaRef = useRef(null); + const viewportRef = useRef(null); + const lastScrollTop = useRef(0); - useEffect(() => { - const timer = setTimeout(() => { - setIsVisible(true); - }, 150); + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, 150); - return () => clearTimeout(timer); - }, []); + return () => clearTimeout(timer); + }, []); - const handleScroll = ({ y }: { y: number }) => { - if (!viewportRef.current) return; + const handleScroll = ({ y }: { y: number }) => { + if (!viewportRef.current) return; - const { scrollHeight, clientHeight } = viewportRef.current; - const isAtBottomNow = y + clientHeight >= scrollHeight - 10; + const { scrollHeight, clientHeight } = viewportRef.current; + const isAtBottomNow = y + clientHeight >= scrollHeight - 10; - if (y < lastScrollTop.current) { - setIsUserScrolling(true); - setShouldAutoScroll(false); - } else if (isAtBottomNow) { - setIsUserScrolling(false); - setShouldAutoScroll(true); - } + if (y < lastScrollTop.current) { + setIsUserScrolling(true); + setShouldAutoScroll(false); + } else if (isAtBottomNow) { + setIsUserScrolling(false); + setShouldAutoScroll(true); + } - lastScrollTop.current = y; - }; + lastScrollTop.current = y; + }; - useEffect(() => { - if (shouldAutoScroll && !isUserScrolling && viewportRef.current) { - const viewport = viewportRef.current; - viewport.scrollTop = viewport.scrollHeight; - } - }, [terminalContent, shouldAutoScroll, isUserScrolling]); + useEffect(() => { + if (shouldAutoScroll && !isUserScrolling && viewportRef.current) { + const viewport = viewportRef.current; + viewport.scrollTop = viewport.scrollHeight; + } + }, [terminalContent, shouldAutoScroll, isUserScrolling]); - useEffect(() => { - const cleanup = window.electronAPI.kobold.onKoboldOutput( - (data: string) => { - setTerminalContent((prev) => { - const newData = data.toString(); + useEffect(() => { + const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => { + setTerminalContent((prev) => handleTerminalOutput(prev, data.toString())); + }); - if (onServerReady) { - const effectiveFrontend = isTextMode - ? frontendPreference - : imageGenerationFrontendPreference === 'builtin' - ? 'koboldcpp' - : frontendPreference; + return cleanup; + }, []); - let signalToCheck: string = SERVER_READY_SIGNALS.KOBOLDCPP; + const scrollToBottom = () => { + if (viewportRef.current) { + const viewport = viewportRef.current; + viewport.scrollTop = viewport.scrollHeight; + setShouldAutoScroll(true); + setIsUserScrolling(false); + } + }; - if (effectiveFrontend === 'sillytavern') { - signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN; - } else if (effectiveFrontend === 'openwebui') { - signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI; - } + useImperativeHandle(ref, () => ({ + scrollToBottom, + })); - if (newData.includes(signalToCheck)) { - setTimeout(() => onServerReady(), 3000); - } - } - - return handleTerminalOutput(prev, newData); - }); - } - ); - - return cleanup; - }, [ - onServerReady, - frontendPreference, - imageGenerationFrontendPreference, - isImageGenerationMode, - isTextMode, - ]); - - const scrollToBottom = () => { - if (viewportRef.current) { - const viewport = viewportRef.current; - viewport.scrollTop = viewport.scrollHeight; - setShouldAutoScroll(true); - setIsUserScrolling(false); - } - }; - - useImperativeHandle(ref, () => ({ - scrollToBottom, - })); - - return ( - + - - -
- - - - {isUserScrolling && !shouldAutoScroll && ( - +
- - - )} - - ); - } -); + dangerouslySetInnerHTML={{ + __html: processTerminalContent(terminalContent), + }} + /> + + + + {isUserScrolling && !shouldAutoScroll && ( + + + + )} + + ); +}); TerminalTab.displayName = 'TerminalTab'; diff --git a/src/components/screens/Interface/index.tsx b/src/components/screens/Interface/index.tsx index 792948c..b640585 100644 --- a/src/components/screens/Interface/index.tsx +++ b/src/components/screens/Interface/index.tsx @@ -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(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', }} > - +
); diff --git a/src/components/screens/Launch/AdvancedTab.tsx b/src/components/screens/Launch/AdvancedTab.tsx index 9105d68..895eacd 100644 --- a/src/components/screens/Launch/AdvancedTab.tsx +++ b/src/components/screens/Launch/AdvancedTab.tsx @@ -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 = () => { handleNoshiftChange(!checked)} + onChange={(checked) => setNoshift(!checked)} label="Context Shift" tooltip="Use Context Shifting to reduce reprocessing." /> { { { { @@ -181,7 +181,7 @@ export const AdvancedTab = () => { handleMoeexpertsChange(Number(value))} + onChange={(value) => setMoeexperts(Number(value))} min={-1} max={128} step={1} @@ -198,7 +198,7 @@ export const AdvancedTab = () => { 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) } /> @@ -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={} onClick={() => { - handlePreLaunchCommandsChange([...preLaunchCommands, '']); + setPreLaunchCommands([...preLaunchCommands, '']); }} style={{ alignSelf: 'flex-start' }} > diff --git a/src/components/screens/Launch/GeneralTab/AccelerationSelector.tsx b/src/components/screens/Launch/GeneralTab/AccelerationSelector.tsx index f21c6e1..0d33256 100644 --- a/src/components/screens/Launch/GeneralTab/AccelerationSelector.tsx +++ b/src/components/screens/Launch/GeneralTab/AccelerationSelector.tsx @@ -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} diff --git a/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx b/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx index aab096b..6118f0a 100644 --- a/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx +++ b/src/components/screens/Launch/GeneralTab/GpuDeviceSelector.tsx @@ -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 = ({ - handleTensorSplitChange(event.target.value) - } + onChange={(event) => setTensorSplit(event.target.value)} size="sm" /> diff --git a/src/components/screens/Launch/GeneralTab/index.tsx b/src/components/screens/Launch/GeneralTab/index.tsx index c6a2b6a..9afc915 100644 --- a/src/components/screens/Launch/GeneralTab/index.tsx +++ b/src/components/screens/Launch/GeneralTab/index.tsx @@ -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 ( { 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) => { - 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' }} /> diff --git a/src/components/screens/Launch/ImageGenerationTab.tsx b/src/components/screens/Launch/ImageGenerationTab.tsx index d0d8ed3..11c808e 100644 --- a/src/components/screens/Launch/ImageGenerationTab.tsx +++ b/src/components/screens/Launch/ImageGenerationTab.tsx @@ -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(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} /> diff --git a/src/components/screens/Launch/NetworkTab.tsx b/src/components/screens/Launch/NetworkTab.tsx index 0803d38..5e09d66 100644 --- a/src/components/screens/Launch/NetworkTab.tsx +++ b/src/components/screens/Launch/NetworkTab.tsx @@ -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 = () => { handleHostChange(event.currentTarget.value)} + onChange={(event) => setHost(event.currentTarget.value)} /> @@ -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 = () => { @@ -99,14 +99,14 @@ export const NetworkTab = () => { @@ -114,7 +114,7 @@ export const NetworkTab = () => { diff --git a/src/components/screens/Launch/index.tsx b/src/components/screens/Launch/index.tsx index 2f67271..5d67ffb 100644 --- a/src/components/screens/Launch/index.tsx +++ b/src/components/screens/Launch/index.tsx @@ -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(() => { diff --git a/src/constants/index.ts b/src/constants/index.ts index 9fff1d3..bd5f252 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -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; diff --git a/src/hooks/useInterfaceSelection.ts b/src/hooks/useInterfaceSelection.ts deleted file mode 100644 index 199e850..0000000 --- a/src/hooks/useInterfaceSelection.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import type { FrontendPreference } from '@/types'; - -export function useFrontendPreference() { - const [frontendPreference, setFrontendPreferenceState] = - useState('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, - }; -} diff --git a/src/hooks/useLaunchConfig.ts b/src/hooks/useLaunchConfig.ts deleted file mode 100644 index 25989d4..0000000 --- a/src/hooks/useLaunchConfig.ts +++ /dev/null @@ -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, - }; -}; diff --git a/src/main/modules/koboldcpp/launcher/index.ts b/src/main/modules/koboldcpp/launcher/index.ts index aa0bddb..1a09f14 100644 --- a/src/main/modules/koboldcpp/launcher/index.ts +++ b/src/main/modules/koboldcpp/launcher/index.ts @@ -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 }); }; diff --git a/src/main/modules/koboldcpp/model-download.ts b/src/main/modules/koboldcpp/model-download.ts index 69c865e..69ec247 100644 --- a/src/main/modules/koboldcpp/model-download.ts +++ b/src/main/modules/koboldcpp/model-download.ts @@ -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, diff --git a/src/main/modules/koboldcpp/tunnel.ts b/src/main/modules/koboldcpp/tunnel.ts index 5b60852..ba5dffd 100644 --- a/src/main/modules/koboldcpp/tunnel.ts +++ b/src/main/modules/koboldcpp/tunnel.ts @@ -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, }); diff --git a/src/main/modules/openwebui.ts b/src/main/modules/openwebui.ts index df18f11..8e110f2 100644 --- a/src/main/modules/openwebui.ts +++ b/src/main/modules/openwebui.ts @@ -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) { async function waitForOpenWebUIToStart() { return new Promise((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')); } }); } diff --git a/src/main/modules/sillytavern.ts b/src/main/modules/sillytavern.ts index b1c3190..29a34b8 100644 --- a/src/main/modules/sillytavern.ts +++ b/src/main/modules/sillytavern.ts @@ -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) { diff --git a/src/preload/index.ts b/src/preload/index.ts index 29eb854..c330ee0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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); diff --git a/src/stores/launchConfig.ts b/src/stores/launchConfig.ts index 5376148..7974028 100644 --- a/src/stores/launchConfig.ts +++ b/src/stores/launchConfig.ts @@ -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; - selectModelFile: () => Promise; - selectSdmodelFile: () => Promise; - selectSdt5xxlFile: () => Promise; - selectSdcliplFile: () => Promise; - selectSdclipgFile: () => Promise; - selectSdphotomakerFile: () => Promise; - selectSdvaeFile: () => Promise; - selectSdloraFile: () => Promise; - contextSizeChangeWithStep: (size: number) => void; - applyImageModelPreset: (preset: ImageModelPreset) => void; + selectFile: ( + field: + | 'model' + | 'sdmodel' + | 'sdt5xxl' + | 'sdclipl' + | 'sdclipg' + | 'sdphotomaker' + | 'sdvae' + | 'sdlora', + title: string + ) => Promise; + setContextSizeWithStep: (size: number) => void; + applyPreset: (presetName: string) => void; } export const useLaunchConfigStore = create((set, get) => ({ @@ -445,98 +449,32 @@ export const useLaunchConfigStore = create((set, get) => ({ return null; }, - selectModelFile: async () => { - const result = await window.electronAPI.kobold.selectModelFile( - 'Select a Text Model File' - ); - if (result) { + 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 }); + }, + + setContextSizeWithStep: (size: number) => { + const rounded = Math.round(size / 256) * 256; + set({ contextSize: Math.max(256, Math.min(131072, rounded)) }); + }, + + applyPreset: (presetName: string) => { + const preset = IMAGE_MODEL_PRESETS.find((p) => p.name === presetName); + if (preset) { set({ - model: result, - isTextMode: Boolean(result?.trim()), + sdmodel: preset.sdmodel, + isImageGenerationMode: Boolean(preset.sdmodel?.trim()), + sdt5xxl: preset.sdt5xxl, + sdclipl: preset.sdclipl, + sdclipg: preset.sdclipg || '', + sdvae: preset.sdvae, + model: '', }); } }, - - selectSdmodelFile: async () => { - const result = await window.electronAPI.kobold.selectModelFile( - 'Select a Image Gen. Model File' - ); - if (result) { - set({ - sdmodel: result, - isImageGenerationMode: Boolean(result?.trim()), - }); - } - }, - - 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) => { - set({ - sdmodel: preset.sdmodel, - isImageGenerationMode: Boolean(preset.sdmodel?.trim()), - sdt5xxl: preset.sdt5xxl, - sdclipl: preset.sdclipl, - sdclipg: preset.sdclipg || '', - sdvae: preset.sdvae, - model: '', - }); - }, })); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index cbe7443..fde9c99 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -180,6 +180,7 @@ export interface KoboldAPI { onKoboldCrashed: ( callback: (crashInfo: KoboldCrashInfo) => void ) => () => void; + onServerReady: (callback: () => void) => () => void; onTunnelUrlChanged: (callback: (url: string | null) => void) => () => void; } diff --git a/src/types/ipc.d.ts b/src/types/ipc.d.ts index f984177..6693a85 100644 --- a/src/types/ipc.d.ts +++ b/src/types/ipc.d.ts @@ -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': [];