mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
refactor code for simplicity, tunnel custom frontend server
This commit is contained in:
parent
d39a26477a
commit
0576b46b29
23 changed files with 391 additions and 618 deletions
|
|
@ -9,20 +9,20 @@ interface AppRouterProps {
|
|||
currentScreen: Screen | null;
|
||||
hasInitialized: boolean;
|
||||
activeInterfaceTab: InterfaceTab;
|
||||
isServerReady: boolean;
|
||||
onWelcomeComplete: () => void;
|
||||
onDownloadComplete: () => void;
|
||||
onLaunch: () => void;
|
||||
onTabChange: (tab: InterfaceTab) => void;
|
||||
}
|
||||
|
||||
export const AppRouter = ({
|
||||
currentScreen,
|
||||
hasInitialized,
|
||||
activeInterfaceTab,
|
||||
isServerReady,
|
||||
onWelcomeComplete,
|
||||
onDownloadComplete,
|
||||
onLaunch,
|
||||
onTabChange,
|
||||
}: AppRouterProps) => {
|
||||
const isInterfaceScreen = currentScreen === 'interface';
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ export const AppRouter = ({
|
|||
>
|
||||
<InterfaceScreen
|
||||
activeTab={activeInterfaceTab}
|
||||
onTabChange={onTabChange}
|
||||
isServerReady={isServerReady}
|
||||
/>
|
||||
</ScreenTransition>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
AppShell,
|
||||
Loader,
|
||||
|
|
@ -19,6 +19,7 @@ import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
|||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import { getDefaultInterfaceTab } from '@/utils/interface';
|
||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
import type { InterfaceTab, Screen } from '@/types';
|
||||
|
|
@ -29,14 +30,45 @@ export const App = () => {
|
|||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const [activeInterfaceTab, setActiveInterfaceTab] =
|
||||
useState<InterfaceTab>('terminal');
|
||||
const [isServerReady, setIsServerReady] = useState(false);
|
||||
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
|
||||
const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null);
|
||||
const isInterfaceScreen = currentScreen === 'interface';
|
||||
|
||||
const { resolvedColorScheme: appColorScheme, systemMonitoringEnabled } =
|
||||
usePreferencesStore();
|
||||
const {
|
||||
resolvedColorScheme: appColorScheme,
|
||||
systemMonitoringEnabled,
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
} = usePreferencesStore();
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const { model, sdmodel } = useLaunchConfigStore();
|
||||
const { model, sdmodel, isTextMode, isImageGenerationMode } =
|
||||
useLaunchConfigStore();
|
||||
|
||||
const defaultInterfaceTab = useMemo(
|
||||
() =>
|
||||
getDefaultInterfaceTab({
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
isTextMode,
|
||||
isImageGenerationMode,
|
||||
}),
|
||||
[
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
isTextMode,
|
||||
isImageGenerationMode,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = window.electronAPI.kobold.onServerReady(() => {
|
||||
setIsServerReady(true);
|
||||
setActiveInterfaceTab(defaultInterfaceTab);
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [defaultInterfaceTab]);
|
||||
|
||||
useEffect(() => {
|
||||
setColorScheme(appColorScheme);
|
||||
|
|
@ -63,6 +95,8 @@ export const App = () => {
|
|||
|
||||
const performEject = () => {
|
||||
window.electronAPI.kobold.stopKoboldCpp();
|
||||
setIsServerReady(false);
|
||||
setActiveInterfaceTab('terminal');
|
||||
setCurrentScreen('launch');
|
||||
};
|
||||
|
||||
|
|
@ -235,10 +269,10 @@ export const App = () => {
|
|||
currentScreen={currentScreen}
|
||||
hasInitialized={hasInitialized}
|
||||
activeInterfaceTab={activeInterfaceTab}
|
||||
isServerReady={isServerReady}
|
||||
onWelcomeComplete={handleWelcomeComplete}
|
||||
onDownloadComplete={handleDownloadComplete}
|
||||
onLaunch={handleLaunch}
|
||||
onTabChange={setActiveInterfaceTab}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -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<TerminalTabRef, TerminalTabProps>(
|
||||
({ 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<HTMLDivElement>(null);
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const lastScrollTop = useRef(0);
|
||||
export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
||||
const [terminalContent, setTerminalContent] = useState('');
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const viewportRef = useRef<HTMLDivElement>(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 (
|
||||
<Box
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-filled)'
|
||||
: 'var(--mantine-color-gray-0)',
|
||||
borderRadius: 'inherit',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<ScrollArea
|
||||
ref={scrollAreaRef}
|
||||
viewportRef={viewportRef}
|
||||
onScrollPositionChange={handleScroll}
|
||||
style={{
|
||||
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-dark-filled)'
|
||||
: 'var(--mantine-color-gray-0)',
|
||||
borderRadius: 'inherit',
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
scrollbarSize={8}
|
||||
offsetScrollbars={false}
|
||||
>
|
||||
<ScrollArea
|
||||
ref={scrollAreaRef}
|
||||
viewportRef={viewportRef}
|
||||
onScrollPositionChange={handleScroll}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
scrollbarSize={8}
|
||||
offsetScrollbars={false}
|
||||
>
|
||||
<Box p="md">
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
color:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-filled)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: processTerminalContent(terminalContent),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
|
||||
{isUserScrolling && !shouldAutoScroll && (
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
size="lg"
|
||||
radius="xl"
|
||||
onClick={scrollToBottom}
|
||||
<Box p="md">
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '1.25rem',
|
||||
right: '1.25rem',
|
||||
zIndex: 10,
|
||||
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
margin: 0,
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: 1.4,
|
||||
color:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-gray-0)'
|
||||
: 'var(--mantine-color-dark-filled)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
}}
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<ChevronDown size={20} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: processTerminalContent(terminalContent),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
|
||||
{isUserScrolling && !shouldAutoScroll && (
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
size="lg"
|
||||
radius="xl"
|
||||
onClick={scrollToBottom}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '1.25rem',
|
||||
right: '1.25rem',
|
||||
zIndex: 10,
|
||||
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<ChevronDown size={20} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
TerminalTab.displayName = 'TerminalTab';
|
||||
|
|
|
|||
|
|
@ -1,54 +1,22 @@
|
|||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
||||
import {
|
||||
TerminalTab,
|
||||
type TerminalTabRef,
|
||||
} from '@/components/screens/Interface/TerminalTab';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { getDefaultInterfaceTab } from '@/utils/interface';
|
||||
import type { InterfaceTab } from '@/types';
|
||||
|
||||
interface InterfaceScreenProps {
|
||||
activeTab?: InterfaceTab | null;
|
||||
onTabChange?: (tab: InterfaceTab) => void;
|
||||
isServerReady: boolean;
|
||||
}
|
||||
|
||||
export const InterfaceScreen = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
isServerReady,
|
||||
}: InterfaceScreenProps) => {
|
||||
const [isServerReady, setIsServerReady] = useState(false);
|
||||
const terminalTabRef = useRef<TerminalTabRef>(null);
|
||||
|
||||
const { isTextMode, isImageGenerationMode } = useLaunchConfigStore();
|
||||
const { frontendPreference, imageGenerationFrontendPreference } =
|
||||
usePreferencesStore();
|
||||
|
||||
const defaultInterfaceTab = useMemo(
|
||||
() =>
|
||||
getDefaultInterfaceTab({
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
isTextMode,
|
||||
isImageGenerationMode,
|
||||
}),
|
||||
[
|
||||
frontendPreference,
|
||||
imageGenerationFrontendPreference,
|
||||
isTextMode,
|
||||
isImageGenerationMode,
|
||||
]
|
||||
);
|
||||
|
||||
const handleServerReady = useCallback(() => {
|
||||
setIsServerReady(true);
|
||||
|
||||
if (onTabChange) {
|
||||
onTabChange(defaultInterfaceTab);
|
||||
}
|
||||
}, [onTabChange, defaultInterfaceTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'terminal' && terminalTabRef.current) {
|
||||
terminalTabRef.current.scrollToBottom();
|
||||
|
|
@ -84,7 +52,7 @@ export const InterfaceScreen = ({
|
|||
display: activeTab === 'terminal' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<TerminalTab ref={terminalTabRef} onServerReady={handleServerReady} />
|
||||
<TerminalTab ref={terminalTabRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { Plus, Trash2 } from 'lucide-react';
|
|||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
|
||||
export const AdvancedTab = () => {
|
||||
const {
|
||||
|
|
@ -30,19 +30,19 @@ export const AdvancedTab = () => {
|
|||
backend,
|
||||
moecpu,
|
||||
moeexperts,
|
||||
handleAdditionalArgumentsChange,
|
||||
handlePreLaunchCommandsChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleLowvramChange,
|
||||
handleQuantmatmulChange,
|
||||
handleUsemmapChange,
|
||||
handleDebugmodeChange,
|
||||
handleMoecpuChange,
|
||||
handleMoeexpertsChange,
|
||||
} = useLaunchConfig();
|
||||
setAdditionalArguments,
|
||||
setPreLaunchCommands,
|
||||
setNoshift,
|
||||
setFlashattention,
|
||||
setNoavx2,
|
||||
setFailsafe,
|
||||
setLowvram,
|
||||
setQuantmatmul,
|
||||
setUsemmap,
|
||||
setDebugmode,
|
||||
setMoecpu,
|
||||
setMoeexperts,
|
||||
} = useLaunchConfigStore();
|
||||
const [commandLineModalOpen, setCommandLineModalOpen] = useState(false);
|
||||
const [backendSupport, setBackendSupport] = useState<{
|
||||
noavx2: boolean;
|
||||
|
|
@ -55,7 +55,7 @@ export const AdvancedTab = () => {
|
|||
const updatedArgs = currentArgs
|
||||
? `${currentArgs} ${newArgument}`
|
||||
: newArgument;
|
||||
handleAdditionalArgumentsChange(updatedArgs);
|
||||
setAdditionalArguments(updatedArgs);
|
||||
};
|
||||
|
||||
const isGpuBackend = backend === 'cuda' || backend === 'rocm';
|
||||
|
|
@ -86,21 +86,21 @@ export const AdvancedTab = () => {
|
|||
<SimpleGrid cols={3} spacing="lg" verticalSpacing="md">
|
||||
<CheckboxWithTooltip
|
||||
checked={!noshift}
|
||||
onChange={(checked) => handleNoshiftChange(!checked)}
|
||||
onChange={(checked) => setNoshift(!checked)}
|
||||
label="Context Shift"
|
||||
tooltip="Use Context Shifting to reduce reprocessing."
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
checked={noshift}
|
||||
onChange={handleNoshiftChange}
|
||||
onChange={setNoshift}
|
||||
label="No Shift"
|
||||
tooltip="Don't use GPU layer shifting for incomplete offloads, which may reduce model performance."
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
checked={noavx2}
|
||||
onChange={handleNoavx2Change}
|
||||
onChange={setNoavx2}
|
||||
label="Disable AVX2"
|
||||
tooltip={
|
||||
!backendSupport?.noavx2 && !isLoading
|
||||
|
|
@ -112,14 +112,14 @@ export const AdvancedTab = () => {
|
|||
|
||||
<CheckboxWithTooltip
|
||||
checked={usemmap}
|
||||
onChange={handleUsemmapChange}
|
||||
onChange={setUsemmap}
|
||||
label="MMAP"
|
||||
tooltip="Use MMAP to load models when enabled."
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
checked={quantmatmul && isGpuBackend}
|
||||
onChange={handleQuantmatmulChange}
|
||||
onChange={setQuantmatmul}
|
||||
label="QuantMatMul"
|
||||
tooltip={
|
||||
!isGpuBackend
|
||||
|
|
@ -131,7 +131,7 @@ export const AdvancedTab = () => {
|
|||
|
||||
<CheckboxWithTooltip
|
||||
checked={failsafe}
|
||||
onChange={handleFailsafeChange}
|
||||
onChange={setFailsafe}
|
||||
label="Failsafe"
|
||||
tooltip={
|
||||
!backendSupport?.failsafe && !isLoading
|
||||
|
|
@ -143,14 +143,14 @@ export const AdvancedTab = () => {
|
|||
|
||||
<CheckboxWithTooltip
|
||||
checked={flashattention}
|
||||
onChange={handleFlashattentionChange}
|
||||
onChange={setFlashattention}
|
||||
label="Flash Attention"
|
||||
tooltip="Enable flash attention to reduce memory usage. May produce incorrect answers for some prompts, but improves performance."
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
checked={lowvram && isGpuBackend}
|
||||
onChange={handleLowvramChange}
|
||||
onChange={setLowvram}
|
||||
label="Low VRAM"
|
||||
tooltip={
|
||||
!isGpuBackend
|
||||
|
|
@ -162,7 +162,7 @@ export const AdvancedTab = () => {
|
|||
|
||||
<CheckboxWithTooltip
|
||||
checked={debugmode}
|
||||
onChange={handleDebugmodeChange}
|
||||
onChange={setDebugmode}
|
||||
label="Debug Mode"
|
||||
tooltip="Shows additional debug info in the terminal."
|
||||
/>
|
||||
|
|
@ -181,7 +181,7 @@ export const AdvancedTab = () => {
|
|||
</Group>
|
||||
<NumberInput
|
||||
value={moeexperts}
|
||||
onChange={(value) => handleMoeexpertsChange(Number(value))}
|
||||
onChange={(value) => setMoeexperts(Number(value))}
|
||||
min={-1}
|
||||
max={128}
|
||||
step={1}
|
||||
|
|
@ -198,7 +198,7 @@ export const AdvancedTab = () => {
|
|||
</Group>
|
||||
<NumberInput
|
||||
value={moecpu}
|
||||
onChange={(value) => handleMoecpuChange(Number(value) || 0)}
|
||||
onChange={(value) => setMoecpu(Number(value) || 0)}
|
||||
min={0}
|
||||
max={999}
|
||||
step={1}
|
||||
|
|
@ -229,7 +229,7 @@ export const AdvancedTab = () => {
|
|||
placeholder="Additional command line arguments"
|
||||
value={additionalArguments}
|
||||
onChange={(event) =>
|
||||
handleAdditionalArgumentsChange(event.currentTarget.value)
|
||||
setAdditionalArguments(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -250,7 +250,7 @@ export const AdvancedTab = () => {
|
|||
onChange={(event) => {
|
||||
const newCommands = [...preLaunchCommands];
|
||||
newCommands[index] = event.currentTarget.value;
|
||||
handlePreLaunchCommandsChange(newCommands);
|
||||
setPreLaunchCommands(newCommands);
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
|
|
@ -262,7 +262,7 @@ export const AdvancedTab = () => {
|
|||
const newCommands = preLaunchCommands.filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
handlePreLaunchCommandsChange(
|
||||
setPreLaunchCommands(
|
||||
newCommands.length === 0 ? [''] : newCommands
|
||||
);
|
||||
}}
|
||||
|
|
@ -276,7 +276,7 @@ export const AdvancedTab = () => {
|
|||
size="xs"
|
||||
leftSection={<Plus size={14} />}
|
||||
onClick={() => {
|
||||
handlePreLaunchCommandsChange([...preLaunchCommands, '']);
|
||||
setPreLaunchCommands([...preLaunchCommands, '']);
|
||||
}}
|
||||
style={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { Text, Group, TextInput } from '@mantine/core';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
import { Select } from '@/components/Select';
|
||||
import type { AccelerationOption } from '@/types';
|
||||
|
||||
const GPU_BACKENDS = ['cuda', 'rocm', 'vulkan', 'clblast'];
|
||||
const TENSOR_SPLIT_BACKENDS = ['cuda', 'rocm', 'vulkan'];
|
||||
|
||||
interface GpuDeviceSelectorProps {
|
||||
availableAccelerations: AccelerationOption[];
|
||||
}
|
||||
|
|
@ -15,18 +18,14 @@ export const GpuDeviceSelector = ({
|
|||
backend,
|
||||
gpuDeviceSelection,
|
||||
tensorSplit,
|
||||
handleGpuDeviceSelectionChange,
|
||||
handleTensorSplitChange,
|
||||
} = useLaunchConfig();
|
||||
setGpuDeviceSelection,
|
||||
setTensorSplit,
|
||||
} = useLaunchConfigStore();
|
||||
|
||||
const selectedAcceleration = availableAccelerations.find(
|
||||
(a) => a.value === backend
|
||||
);
|
||||
const isGpuAcceleration =
|
||||
backend === 'cuda' ||
|
||||
backend === 'rocm' ||
|
||||
backend === 'vulkan' ||
|
||||
backend === 'clblast';
|
||||
const isGpu = GPU_BACKENDS.includes(backend);
|
||||
|
||||
const getDiscreteDeviceCount = () => {
|
||||
if (!selectedAcceleration?.devices) return 0;
|
||||
|
|
@ -40,11 +39,11 @@ export const GpuDeviceSelector = ({
|
|||
|
||||
const hasMultipleDevices = getDiscreteDeviceCount() > 1;
|
||||
const showTensorSplit =
|
||||
(backend === 'cuda' || backend === 'rocm' || backend === 'vulkan') &&
|
||||
TENSOR_SPLIT_BACKENDS.includes(backend) &&
|
||||
hasMultipleDevices &&
|
||||
gpuDeviceSelection === 'all';
|
||||
|
||||
if (!isGpuAcceleration || !hasMultipleDevices) {
|
||||
if (!isGpu || !hasMultipleDevices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +118,7 @@ export const GpuDeviceSelector = ({
|
|||
value={gpuDeviceSelection}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
handleGpuDeviceSelectionChange(value);
|
||||
setGpuDeviceSelection(value);
|
||||
}
|
||||
}}
|
||||
data={deviceOptions}
|
||||
|
|
@ -138,9 +137,7 @@ export const GpuDeviceSelector = ({
|
|||
<TextInput
|
||||
placeholder="e.g., 3,2 or 1,1,1"
|
||||
value={tensorSplit}
|
||||
onChange={(event) =>
|
||||
handleTensorSplitChange(event.target.value)
|
||||
}
|
||||
onChange={(event) => setTensorSplit(event.target.value)}
|
||||
size="sm"
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,20 +9,15 @@ import {
|
|||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector';
|
||||
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
|
||||
interface GeneralTabProps {
|
||||
configLoaded?: boolean;
|
||||
}
|
||||
|
||||
export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||
const {
|
||||
model,
|
||||
contextSize,
|
||||
handleModelChange,
|
||||
handleSelectModelFile,
|
||||
handleContextSizeChangeWithStep,
|
||||
} = useLaunchConfig();
|
||||
const { model, contextSize, setModel, selectFile, setContextSizeWithStep } =
|
||||
useLaunchConfigStore();
|
||||
|
||||
return (
|
||||
<Transition
|
||||
|
|
@ -40,8 +35,8 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
|||
value={model}
|
||||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||
tooltip="Select a GGUF text generation model file for chat and completion tasks."
|
||||
onChange={handleModelChange}
|
||||
onSelectFile={handleSelectModelFile}
|
||||
onChange={setModel}
|
||||
onSelectFile={() => selectFile('model', 'Select Text Model')}
|
||||
searchUrl="https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending"
|
||||
showAnalyze
|
||||
paramType="model"
|
||||
|
|
@ -58,9 +53,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
|||
<TextInput
|
||||
value={contextSize?.toString() || ''}
|
||||
onChange={(event) =>
|
||||
handleContextSizeChangeWithStep(
|
||||
Number(event.target.value) || 256
|
||||
)
|
||||
setContextSizeWithStep(Number(event.target.value) || 256)
|
||||
}
|
||||
type="number"
|
||||
min={256}
|
||||
|
|
@ -75,7 +68,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
|||
min={256}
|
||||
max={131072}
|
||||
step={1}
|
||||
onChange={handleContextSizeChangeWithStep}
|
||||
onChange={setContextSizeWithStep}
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
|||
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
|
||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
|
||||
export const ImageGenerationTab = () => {
|
||||
const {
|
||||
|
|
@ -18,25 +18,19 @@ export const ImageGenerationTab = () => {
|
|||
sdconvdirect,
|
||||
sdvaecpu,
|
||||
sdclipgpu,
|
||||
handleSdmodelChange,
|
||||
handleSdt5xxlChange,
|
||||
handleSdcliplChange,
|
||||
handleSdclipgChange,
|
||||
handleSdphotomakerChange,
|
||||
handleSdvaeChange,
|
||||
handleSdloraChange,
|
||||
handleSdconvdirectChange,
|
||||
handleSdvaecpuChange,
|
||||
handleSdclipgpuChange,
|
||||
handleApplyPreset,
|
||||
handleSelectSdmodelFile,
|
||||
handleSelectSdt5xxlFile,
|
||||
handleSelectSdcliplFile,
|
||||
handleSelectSdclipgFile,
|
||||
handleSelectSdphotomakerFile,
|
||||
handleSelectSdvaeFile,
|
||||
handleSelectSdloraFile,
|
||||
} = useLaunchConfig();
|
||||
setSdmodel,
|
||||
setSdt5xxl,
|
||||
setSdclipl,
|
||||
setSdclipg,
|
||||
setSdphotomaker,
|
||||
setSdvae,
|
||||
setSdlora,
|
||||
setSdconvdirect,
|
||||
setSdvaecpu,
|
||||
setSdclipgpu,
|
||||
applyPreset,
|
||||
selectFile,
|
||||
} = useLaunchConfigStore();
|
||||
|
||||
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -54,15 +48,15 @@ export const ImageGenerationTab = () => {
|
|||
onChange={(value) => {
|
||||
setSelectedPreset(value);
|
||||
if (value) {
|
||||
handleApplyPreset(value);
|
||||
applyPreset(value);
|
||||
} else {
|
||||
handleSdmodelChange('');
|
||||
handleSdt5xxlChange('');
|
||||
handleSdcliplChange('');
|
||||
handleSdclipgChange('');
|
||||
handleSdphotomakerChange('');
|
||||
handleSdvaeChange('');
|
||||
handleSdloraChange('');
|
||||
setSdmodel('');
|
||||
setSdt5xxl('');
|
||||
setSdclipl('');
|
||||
setSdclipg('');
|
||||
setSdphotomaker('');
|
||||
setSdvae('');
|
||||
setSdlora('');
|
||||
}
|
||||
}}
|
||||
clearable
|
||||
|
|
@ -73,8 +67,8 @@ export const ImageGenerationTab = () => {
|
|||
value={sdmodel}
|
||||
placeholder="Select a model file or enter a direct URL"
|
||||
tooltip="The primary image generation model. This is the main model that will generate images."
|
||||
onChange={handleSdmodelChange}
|
||||
onSelectFile={handleSelectSdmodelFile}
|
||||
onChange={setSdmodel}
|
||||
onSelectFile={() => selectFile('sdmodel', 'Select Image Model')}
|
||||
searchUrl="https://huggingface.co/models?pipeline_tag=text-to-image&library=gguf&sort=trending"
|
||||
showAnalyze
|
||||
paramType="sdmodel"
|
||||
|
|
@ -85,8 +79,8 @@ export const ImageGenerationTab = () => {
|
|||
value={sdt5xxl}
|
||||
placeholder="Select a T5-XXL encoder file or enter a direct URL"
|
||||
tooltip="T5-XXL text encoder model for advanced text understanding."
|
||||
onChange={handleSdt5xxlChange}
|
||||
onSelectFile={handleSelectSdt5xxlFile}
|
||||
onChange={setSdt5xxl}
|
||||
onSelectFile={() => selectFile('sdt5xxl', 'Select T5XXL Model')}
|
||||
searchUrl="https://huggingface.co/models?search=t5-xxl&library=gguf&sort=trending"
|
||||
paramType="sdt5xxl"
|
||||
/>
|
||||
|
|
@ -96,8 +90,8 @@ export const ImageGenerationTab = () => {
|
|||
value={sdclipl}
|
||||
placeholder="Select a Clip-L file or enter a direct URL"
|
||||
tooltip="CLIP-L text encoder model for text-image understanding."
|
||||
onChange={handleSdcliplChange}
|
||||
onSelectFile={handleSelectSdcliplFile}
|
||||
onChange={setSdclipl}
|
||||
onSelectFile={() => selectFile('sdclipl', 'Select CLIP-L Model')}
|
||||
searchUrl="https://huggingface.co/models?search=clip-l&library=gguf&sort=trending"
|
||||
paramType="sdclipl"
|
||||
/>
|
||||
|
|
@ -107,8 +101,8 @@ export const ImageGenerationTab = () => {
|
|||
value={sdclipg}
|
||||
placeholder="Select a Clip-G file or enter a direct URL"
|
||||
tooltip="CLIP-G text encoder model for enhanced text-image understanding."
|
||||
onChange={handleSdclipgChange}
|
||||
onSelectFile={handleSelectSdclipgFile}
|
||||
onChange={setSdclipg}
|
||||
onSelectFile={() => selectFile('sdclipg', 'Select CLIP-G Model')}
|
||||
searchUrl="https://huggingface.co/models?search=clip-g&library=gguf&sort=trending"
|
||||
paramType="sdclipg"
|
||||
/>
|
||||
|
|
@ -118,8 +112,10 @@ export const ImageGenerationTab = () => {
|
|||
value={sdphotomaker}
|
||||
placeholder="Select a PhotoMaker file or enter a direct URL"
|
||||
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
|
||||
onChange={handleSdphotomakerChange}
|
||||
onSelectFile={handleSelectSdphotomakerFile}
|
||||
onChange={setSdphotomaker}
|
||||
onSelectFile={() =>
|
||||
selectFile('sdphotomaker', 'Select PhotoMaker Model')
|
||||
}
|
||||
searchUrl="https://huggingface.co/models?search=photomaker&library=safetensors&sort=trending"
|
||||
paramType="sdphotomaker"
|
||||
/>
|
||||
|
|
@ -129,8 +125,8 @@ export const ImageGenerationTab = () => {
|
|||
value={sdvae}
|
||||
placeholder="Select a VAE file or enter a direct URL"
|
||||
tooltip="Variational Autoencoder model for improved image quality."
|
||||
onChange={handleSdvaeChange}
|
||||
onSelectFile={handleSelectSdvaeFile}
|
||||
onChange={setSdvae}
|
||||
onSelectFile={() => selectFile('sdvae', 'Select VAE Model')}
|
||||
searchUrl="https://huggingface.co/models?search=vae&library=safetensors&sort=trending"
|
||||
paramType="sdvae"
|
||||
/>
|
||||
|
|
@ -140,8 +136,8 @@ export const ImageGenerationTab = () => {
|
|||
value={sdlora}
|
||||
placeholder="Select a LoRa file or enter a direct URL"
|
||||
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
|
||||
onChange={handleSdloraChange}
|
||||
onSelectFile={handleSelectSdloraFile}
|
||||
onChange={setSdlora}
|
||||
onSelectFile={() => selectFile('sdlora', 'Select LoRA Model')}
|
||||
searchUrl="https://huggingface.co/models?search=lora&library=safetensors&sort=trending"
|
||||
paramType="sdlora"
|
||||
/>
|
||||
|
|
@ -152,7 +148,7 @@ export const ImageGenerationTab = () => {
|
|||
value={sdconvdirect}
|
||||
onChange={(value) => {
|
||||
if (value === 'off' || value === 'vaeonly' || value === 'full') {
|
||||
handleSdconvdirectChange(value);
|
||||
setSdconvdirect(value);
|
||||
}
|
||||
}}
|
||||
data={[
|
||||
|
|
@ -167,14 +163,14 @@ export const ImageGenerationTab = () => {
|
|||
label="Force VAE to CPU"
|
||||
tooltip="Forces the VAE (Variational Autoencoder) to run on CPU instead of GPU. This can save VRAM but will be slower. Useful for systems with limited GPU memory."
|
||||
checked={sdvaecpu}
|
||||
onChange={handleSdvaecpuChange}
|
||||
onChange={setSdvaecpu}
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
label="Offload CLIP/T5"
|
||||
tooltip="Offloads CLIP and T5 text encoders to the GPU for faster processing. By default they run on CPU. Only enable if you have VRAM to spare."
|
||||
checked={sdclipgpu}
|
||||
onChange={handleSdclipgpuChange}
|
||||
onChange={setSdclipgpu}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Stack, Text, TextInput, Group } from '@mantine/core';
|
|||
import { useState } from 'react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||
|
||||
export const NetworkTab = () => {
|
||||
const {
|
||||
|
|
@ -13,14 +13,14 @@ export const NetworkTab = () => {
|
|||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
handlePortChange,
|
||||
handleHostChange,
|
||||
handleMultiuserChange,
|
||||
handleMultiplayerChange,
|
||||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
} = useLaunchConfig();
|
||||
setPort,
|
||||
setHost,
|
||||
setMultiuser,
|
||||
setMultiplayer,
|
||||
setRemotetunnel,
|
||||
setNocertify,
|
||||
setWebsearch,
|
||||
} = useLaunchConfigStore();
|
||||
const [portInput, setPortInput] = useState('');
|
||||
|
||||
return (
|
||||
|
|
@ -36,7 +36,7 @@ export const NetworkTab = () => {
|
|||
<TextInput
|
||||
placeholder="localhost"
|
||||
value={host}
|
||||
onChange={(event) => handleHostChange(event.currentTarget.value)}
|
||||
onChange={(event) => setHost(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -60,13 +60,13 @@ export const NetworkTab = () => {
|
|||
|
||||
const numValue = Number(value);
|
||||
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
|
||||
handlePortChange(numValue);
|
||||
setPort(numValue);
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
if (value === '') {
|
||||
handlePortChange(undefined);
|
||||
setPort(undefined);
|
||||
setPortInput('');
|
||||
}
|
||||
}}
|
||||
|
|
@ -83,14 +83,14 @@ export const NetworkTab = () => {
|
|||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<CheckboxWithTooltip
|
||||
checked={multiuser}
|
||||
onChange={handleMultiuserChange}
|
||||
onChange={setMultiuser}
|
||||
label="Multiuser Mode"
|
||||
tooltip="Allows requests by multiple different clients to be queued and handled in sequence."
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
checked={multiplayer}
|
||||
onChange={handleMultiplayerChange}
|
||||
onChange={setMultiplayer}
|
||||
label="Shared Multiplayer"
|
||||
tooltip="Hosts a shared multiplayer session"
|
||||
/>
|
||||
|
|
@ -99,14 +99,14 @@ export const NetworkTab = () => {
|
|||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<CheckboxWithTooltip
|
||||
checked={remotetunnel}
|
||||
onChange={handleRemotetunnelChange}
|
||||
onChange={setRemotetunnel}
|
||||
label="Remote Tunnel"
|
||||
tooltip="Creates a trycloudflare tunnel. Allows you to access your server from other devices over an internet URL."
|
||||
/>
|
||||
|
||||
<CheckboxWithTooltip
|
||||
checked={nocertify}
|
||||
onChange={handleNocertifyChange}
|
||||
onChange={setNocertify}
|
||||
label="No Certify Mode (Insecure)"
|
||||
tooltip="Allows insecure SSL connections. Use this if you have SSL cert errors and need to bypass certificate restrictions."
|
||||
/>
|
||||
|
|
@ -114,7 +114,7 @@ export const NetworkTab = () => {
|
|||
|
||||
<CheckboxWithTooltip
|
||||
checked={websearch}
|
||||
onChange={handleWebsearchChange}
|
||||
onChange={setWebsearch}
|
||||
label="Enable WebSearch"
|
||||
tooltip="Enable the local search engine proxy so Web Searches can be done."
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { app } from 'electron';
|
|||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
import { logError } from '@/utils/node/logging';
|
||||
import { sendKoboldOutput } from './window';
|
||||
import { sendKoboldOutput, sendToRenderer } from './window';
|
||||
import { getInstallDir } from './config';
|
||||
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { terminateProcess } from '@/utils/node/process';
|
||||
|
|
@ -38,22 +38,28 @@ async function createUvProcess(args: string[], env?: Record<string, string>) {
|
|||
|
||||
async function waitForOpenWebUIToStart() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let resolved = false;
|
||||
|
||||
const checkForOutput = (data: Buffer) => {
|
||||
if (resolved) return;
|
||||
const output = data.toString();
|
||||
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
|
||||
resolved = true;
|
||||
sendKoboldOutput('Open WebUI is now running!');
|
||||
sendToRenderer('server-ready');
|
||||
resolve();
|
||||
|
||||
if (openWebUIProcess?.stdout) {
|
||||
openWebUIProcess.stdout.removeListener('data', checkForOutput);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (openWebUIProcess?.stdout) {
|
||||
openWebUIProcess.stdout.on('data', checkForOutput);
|
||||
} else {
|
||||
reject(new Error('Open WebUI process stdout not available'));
|
||||
}
|
||||
if (openWebUIProcess?.stderr) {
|
||||
openWebUIProcess.stderr.on('data', checkForOutput);
|
||||
}
|
||||
|
||||
if (!openWebUIProcess?.stdout && !openWebUIProcess?.stderr) {
|
||||
reject(new Error('Open WebUI process streams not available'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { create } from 'zustand';
|
||||
import type { ConfigFile, SdConvDirectMode } from '@/types';
|
||||
import type { ImageModelPreset } from '@/constants/imageModelPresets';
|
||||
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
|
||||
import { DEFAULT_AUTO_GPU_LAYERS, DEFAULT_CONTEXT_SIZE } from '@/constants';
|
||||
|
||||
interface LaunchConfigState {
|
||||
|
|
@ -87,16 +87,20 @@ interface LaunchConfigState {
|
|||
configFiles: ConfigFile[],
|
||||
savedConfig: string | null
|
||||
) => Promise<string | null>;
|
||||
selectModelFile: () => Promise<void>;
|
||||
selectSdmodelFile: () => Promise<void>;
|
||||
selectSdt5xxlFile: () => Promise<void>;
|
||||
selectSdcliplFile: () => Promise<void>;
|
||||
selectSdclipgFile: () => Promise<void>;
|
||||
selectSdphotomakerFile: () => Promise<void>;
|
||||
selectSdvaeFile: () => Promise<void>;
|
||||
selectSdloraFile: () => Promise<void>;
|
||||
contextSizeChangeWithStep: (size: number) => void;
|
||||
applyImageModelPreset: (preset: ImageModelPreset) => void;
|
||||
selectFile: (
|
||||
field:
|
||||
| 'model'
|
||||
| 'sdmodel'
|
||||
| 'sdt5xxl'
|
||||
| 'sdclipl'
|
||||
| 'sdclipg'
|
||||
| 'sdphotomaker'
|
||||
| 'sdvae'
|
||||
| 'sdlora',
|
||||
title: string
|
||||
) => Promise<void>;
|
||||
setContextSizeWithStep: (size: number) => void;
|
||||
applyPreset: (presetName: string) => void;
|
||||
}
|
||||
|
||||
export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
||||
|
|
@ -445,98 +449,32 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
|
|||
return null;
|
||||
},
|
||||
|
||||
selectModelFile: async () => {
|
||||
const result = await window.electronAPI.kobold.selectModelFile(
|
||||
'Select a Text Model File'
|
||||
);
|
||||
if (result) {
|
||||
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: '',
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
|
|
@ -180,6 +180,7 @@ export interface KoboldAPI {
|
|||
onKoboldCrashed: (
|
||||
callback: (crashInfo: KoboldCrashInfo) => void
|
||||
) => () => void;
|
||||
onServerReady: (callback: () => void) => () => void;
|
||||
onTunnelUrlChanged: (callback: (url: string | null) => void) => () => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
2
src/types/ipc.d.ts
vendored
2
src/types/ipc.d.ts
vendored
|
|
@ -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': [];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue