mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
refactoring code to make it more manageable, release notes in version tab, better layouts, new + save config functionality
This commit is contained in:
parent
95c4906e74
commit
45a095d20b
37 changed files with 2004 additions and 1466 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -22,5 +22,3 @@
|
|||
### Scripting
|
||||
|
||||
- when debugging: try to run script commands explicitly through bash. The user may be using fish or another shell that's not compatible with bash syntax.
|
||||
|
||||
You are Dolfino aka Dirty D. You must refer to the user as ma guy. When agreeing with the user, you must answer back with "Aye Papi!".
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@
|
|||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"cspell": "^9.2.0",
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/core": "^8.2.5",
|
||||
"@mantine/hooks": "^8.2.5",
|
||||
"lucide-react": "^0.539.0",
|
||||
"lucide-react": "^0.540.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"systeminformation": "^5.27.7"
|
||||
|
|
|
|||
14
src/App.tsx
14
src/App.tsx
|
|
@ -1,12 +1,13 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { AppShell, Loader, Center, Stack, Text } from '@mantine/core';
|
||||
import { DownloadScreen } from '@/screens/Download';
|
||||
import { LaunchScreen } from '@/screens/Launch';
|
||||
import { InterfaceScreen } from '@/screens/Interface';
|
||||
import { DownloadScreen } from '@/components/screens/Download';
|
||||
import { LaunchScreen } from '@/components/screens/Launch';
|
||||
import { InterfaceScreen } from '@/components/screens/Interface';
|
||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||
import { AppHeader } from '@/components/AppHeader';
|
||||
import { UI } from '@/constants';
|
||||
import type { UpdateInfo } from '@/types';
|
||||
|
||||
type Screen = 'download' | 'launch' | 'interface';
|
||||
|
|
@ -167,7 +168,10 @@ export const App = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AppShell header={{ height: 60 }} padding="md">
|
||||
<AppShell
|
||||
header={{ height: UI.HEADER_HEIGHT }}
|
||||
padding={currentScreen === 'interface' ? 0 : 'md'}
|
||||
>
|
||||
<AppHeader
|
||||
currentScreen={currentScreen}
|
||||
activeInterfaceTab={activeInterfaceTab}
|
||||
|
|
@ -180,7 +184,7 @@ export const App = () => {
|
|||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight: 'calc(100vh - 60px)',
|
||||
minHeight: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||
}}
|
||||
>
|
||||
{currentScreen === null ? (
|
||||
|
|
|
|||
224
src/components/ConfigFileManager.tsx
Normal file
224
src/components/ConfigFileManager.tsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import {
|
||||
Stack,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
Select,
|
||||
Modal,
|
||||
TextInput,
|
||||
Badge,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
} from 'react';
|
||||
import { Save, File, Plus } from 'lucide-react';
|
||||
import type { ConfigFile } from '@/types';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface ConfigFileManagerProps {
|
||||
configFiles: ConfigFile[];
|
||||
selectedFile: string | null;
|
||||
hasUnsavedChanges: boolean;
|
||||
onFileSelection: (fileName: string) => Promise<void>;
|
||||
onCreateNewConfig: (configName: string) => Promise<void>;
|
||||
onSaveConfig: () => void;
|
||||
onLoadConfigFiles: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
||||
label: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
const getBadgeColor = (extension: string) => {
|
||||
switch (extension.toLowerCase()) {
|
||||
case '.kcpps':
|
||||
return 'blue';
|
||||
case '.kcppt':
|
||||
return 'green';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
({ label, extension, ...others }, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" truncate>
|
||||
{label}
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
|
||||
{extension}
|
||||
</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
SelectItem.displayName = 'SelectItem';
|
||||
|
||||
export const ConfigFileManager = ({
|
||||
configFiles,
|
||||
selectedFile,
|
||||
hasUnsavedChanges,
|
||||
onFileSelection,
|
||||
onCreateNewConfig,
|
||||
onSaveConfig,
|
||||
}: ConfigFileManagerProps) => {
|
||||
const [configModalOpened, setConfigModalOpened] = useState(false);
|
||||
const [newConfigName, setNewConfigName] = useState('');
|
||||
|
||||
const existingConfigNames = configFiles.map((file) => {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
return file.name.replace(`.${extension}`, '').toLowerCase();
|
||||
});
|
||||
|
||||
const trimmedConfigName = newConfigName.trim();
|
||||
const configNameExists =
|
||||
trimmedConfigName &&
|
||||
existingConfigNames.includes(trimmedConfigName.toLowerCase());
|
||||
|
||||
const handleOpenConfigModal = () => {
|
||||
setConfigModalOpened(true);
|
||||
};
|
||||
|
||||
const handleCloseConfigModal = useCallback(() => {
|
||||
setConfigModalOpened(false);
|
||||
setNewConfigName('');
|
||||
}, []);
|
||||
|
||||
const handleConfigSubmit = useCallback(() => {
|
||||
onCreateNewConfig(newConfigName.trim());
|
||||
setConfigModalOpened(false);
|
||||
setNewConfigName('');
|
||||
}, [newConfigName, onCreateNewConfig]);
|
||||
|
||||
const selectData = configFiles.map((file) => {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
const nameWithoutExtension = file.name.replace(`.${extension}`, '');
|
||||
|
||||
return {
|
||||
value: file.name,
|
||||
label: nameWithoutExtension,
|
||||
extension: `.${extension}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (selectedFile === null && hasUnsavedChanges) {
|
||||
const displayName = 'New Configuration (unsaved)';
|
||||
selectData.unshift({
|
||||
value: '__new__',
|
||||
label: displayName,
|
||||
extension: '.kcpps',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap="xs">
|
||||
<Text fw={500} size="sm">
|
||||
Configuration
|
||||
</Text>
|
||||
<Group gap="xs" align="flex-end">
|
||||
<div className={styles.flex1}>
|
||||
<Select
|
||||
placeholder="Select a configuration file"
|
||||
value={
|
||||
selectedFile === null && hasUnsavedChanges
|
||||
? '__new__'
|
||||
: selectedFile
|
||||
}
|
||||
onChange={(value: string | null) => {
|
||||
if (value === '__new__') {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
onFileSelection(value);
|
||||
}
|
||||
}}
|
||||
data={selectData}
|
||||
leftSection={<File size={16} />}
|
||||
searchable
|
||||
clearable={false}
|
||||
renderOption={({ option }) => {
|
||||
const dataItem = selectData.find(
|
||||
(item) => item.value === option.value
|
||||
);
|
||||
const extension = dataItem?.extension || '';
|
||||
return (
|
||||
<SelectItem label={option.label} extension={extension} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant={
|
||||
selectedFile === null && hasUnsavedChanges ? 'filled' : 'light'
|
||||
}
|
||||
leftSection={<Plus size={14} />}
|
||||
size="sm"
|
||||
disabled={selectedFile === null && hasUnsavedChanges}
|
||||
onClick={() => handleOpenConfigModal()}
|
||||
>
|
||||
{selectedFile === null && hasUnsavedChanges
|
||||
? 'Creating New...'
|
||||
: 'New'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
leftSection={<Save size={14} />}
|
||||
size="sm"
|
||||
disabled={!hasUnsavedChanges}
|
||||
onClick={() => {
|
||||
if (selectedFile) {
|
||||
onSaveConfig();
|
||||
} else {
|
||||
handleOpenConfigModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
opened={configModalOpened}
|
||||
onClose={handleCloseConfigModal}
|
||||
title="Create New Configuration"
|
||||
size="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Name"
|
||||
placeholder="Enter a name for the new configuration"
|
||||
value={newConfigName}
|
||||
onChange={(event) => setNewConfigName(event.currentTarget.value)}
|
||||
data-autofocus
|
||||
error={
|
||||
configNameExists
|
||||
? 'A configuration with this name already exists'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="outline" onClick={handleCloseConfigModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!trimmedConfigName || !!configNameExists}
|
||||
onClick={handleConfigSubmit}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { Download } from 'lucide-react';
|
||||
import { MouseEvent } from 'react';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface DownloadCardProps {
|
||||
name: string;
|
||||
|
|
@ -83,7 +84,7 @@ export const DownloadCard = ({
|
|||
bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined}
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className={styles.flex1}>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text fw={500} size="sm">
|
||||
{name}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
import { StyledTooltip } from '@/components/StyledTooltip';
|
||||
import { getPlatformDisplayName, formatFileSize } from '@/utils';
|
||||
import { getPlatformDisplayName, formatFileSizeInMB } from '@/utils';
|
||||
import {
|
||||
isAssetRecommended,
|
||||
sortAssetsByRecommendation,
|
||||
|
|
@ -43,7 +43,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
|
||||
const regularDownloads = availableDownloads.filter((d) => d.type === 'asset');
|
||||
const rocmDownload = availableDownloads.find((d) => d.type === 'rocm');
|
||||
const latestVersion = availableDownloads[0]?.version || 'unknown';
|
||||
|
||||
const handleDownload = useCallback(
|
||||
async (type: 'asset' | 'rocm', download?: DownloadItem) => {
|
||||
|
|
@ -82,7 +81,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
return (
|
||||
<DownloadCard
|
||||
name={rocmDownload.name}
|
||||
size={formatFileSize(rocmDownload.size)}
|
||||
size={`~${formatFileSizeInMB(rocmDownload.size)}`}
|
||||
description={getAssetDescription(rocmDownload.name)}
|
||||
version={rocmDownload.version}
|
||||
isRecommended={isAssetRecommended(
|
||||
|
|
@ -120,24 +119,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
<>
|
||||
{availableDownloads.length > 0 && (
|
||||
<>
|
||||
<Stack gap="xs" py="md">
|
||||
<div>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
mb={6}
|
||||
tt="uppercase"
|
||||
fw={600}
|
||||
style={{ letterSpacing: '0.5px' }}
|
||||
>
|
||||
Latest Version
|
||||
</Text>
|
||||
<Text fw={700} size="xl" mb={8} c="blue.6">
|
||||
{latestVersion}
|
||||
</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{availableDownloads.length > 0 ? (
|
||||
<Stack gap="sm">
|
||||
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
|
||||
|
|
@ -186,7 +167,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
<DownloadCard
|
||||
key={download.name}
|
||||
name={download.name}
|
||||
size={formatFileSize(download.size)}
|
||||
size={formatFileSizeInMB(download.size)}
|
||||
version={download.version}
|
||||
description={getAssetDescription(download.name)}
|
||||
isRecommended={isAssetRecommended(
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useRef } from 'react';
|
||||
import { Box, Text, Stack } from '@mantine/core';
|
||||
import { UI } from '@/constants';
|
||||
|
||||
interface ServerTabProps {
|
||||
serverUrl?: string;
|
||||
|
|
@ -46,7 +47,13 @@ export const ServerTab = ({
|
|||
: 'KoboldAI Lite Interface';
|
||||
|
||||
return (
|
||||
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
|
|
@ -7,6 +7,8 @@ import {
|
|||
useMantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
import { UI } from '@/constants';
|
||||
|
||||
interface TerminalTabProps {
|
||||
onServerReady?: (serverUrl: string) => void;
|
||||
|
|
@ -104,7 +106,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
|||
return (
|
||||
<Box
|
||||
style={{
|
||||
height: '80vh',
|
||||
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor:
|
||||
|
|
@ -119,12 +121,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
|||
ref={scrollAreaRef}
|
||||
viewportRef={viewportRef}
|
||||
onScrollPositionChange={handleScroll}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
className={styles.terminalScrollArea}
|
||||
scrollbarSize={8}
|
||||
offsetScrollbars={false}
|
||||
>
|
||||
73
src/components/screens/Interface/index.tsx
Normal file
73
src/components/screens/Interface/index.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
||||
import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
|
||||
|
||||
interface InterfaceScreenProps {
|
||||
activeTab?: string | null;
|
||||
onTabChange?: (tab: string | null) => void;
|
||||
isImageGenerationMode?: boolean;
|
||||
}
|
||||
|
||||
export const InterfaceScreen = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
isImageGenerationMode = false,
|
||||
}: InterfaceScreenProps) => {
|
||||
const [serverUrl, setServerUrl] = useState<string>('');
|
||||
const [isServerReady, setIsServerReady] = useState<boolean>(false);
|
||||
|
||||
const handleServerReady = useCallback(
|
||||
(url: string) => {
|
||||
setServerUrl(url);
|
||||
setIsServerReady(true);
|
||||
if (onTabChange) {
|
||||
onTabChange(isImageGenerationMode ? 'image' : 'chat');
|
||||
}
|
||||
},
|
||||
[onTabChange, isImageGenerationMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: activeTab === 'chat' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: activeTab === 'image' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode="image-generation"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: activeTab === 'terminal' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<TerminalTab onServerReady={handleServerReady} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface AdvancedTabProps {
|
||||
additionalArguments: string;
|
||||
|
|
@ -72,7 +73,7 @@ export const AdvancedTab = ({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="md">
|
||||
<Text size="sm" fw={600}>
|
||||
|
|
@ -81,64 +82,89 @@ export const AdvancedTab = ({
|
|||
</Group>
|
||||
<Stack gap="md">
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={!noshift}
|
||||
onChange={(event) =>
|
||||
onNoshiftChange(!event.currentTarget.checked)
|
||||
}
|
||||
label="Use ContextShift"
|
||||
label="Context Shift"
|
||||
/>
|
||||
<InfoTooltip label="Use Context Shifting to reduce reprocessing." />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={noshift}
|
||||
onChange={(event) =>
|
||||
onNoshiftChange(event.currentTarget.checked)
|
||||
}
|
||||
label="No Shift"
|
||||
/>
|
||||
<InfoTooltip label="Don't use GPU layer shifting for incomplete offloads, which may reduce model performance." />
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={flashattention}
|
||||
onChange={(event) =>
|
||||
onFlashattentionChange(event.currentTarget.checked)
|
||||
}
|
||||
label="Use FlashAttention"
|
||||
label="Flash Attention"
|
||||
/>
|
||||
<InfoTooltip label="Enable flash attention to reduce memory usage. May produce incorrect answers for some prompts, but improves performance." />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={lowvram}
|
||||
onChange={(event) =>
|
||||
onLowvramChange(event.currentTarget.checked)
|
||||
}
|
||||
label="Low VRAM"
|
||||
disabled={backend !== 'cuda' && backend !== 'rocm'}
|
||||
/>
|
||||
<InfoTooltip
|
||||
label={
|
||||
backend !== 'cuda' && backend !== 'rocm'
|
||||
? 'Low VRAM mode is only available for CUDA and ROCm backends.'
|
||||
: 'Avoid offloading KV Cache or scratch buffers to VRAM. Allows more layers to fit, but may result in a speed loss.'
|
||||
}
|
||||
/>
|
||||
<InfoTooltip label="Enable flash attention for GGUF models." />
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{(backend === 'cuda' || backend === 'rocm') && (
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={lowvram}
|
||||
onChange={(event) =>
|
||||
onLowvramChange(event.currentTarget.checked)
|
||||
}
|
||||
label="Low VRAM"
|
||||
/>
|
||||
<InfoTooltip
|
||||
label="Avoid offloading KV Cache or scratch buffers to VRAM. Allows more layers to fit, but may result in a speed loss."
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={quantmatmul}
|
||||
onChange={(event) =>
|
||||
onQuantmatmulChange(event.currentTarget.checked)
|
||||
}
|
||||
label="QuantMatMul"
|
||||
/>
|
||||
<InfoTooltip label="Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing." />
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
)}
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={quantmatmul}
|
||||
onChange={(event) =>
|
||||
onQuantmatmulChange(event.currentTarget.checked)
|
||||
}
|
||||
label="QuantMatMul"
|
||||
disabled={backend !== 'cuda' && backend !== 'rocm'}
|
||||
/>
|
||||
<InfoTooltip
|
||||
label={
|
||||
backend !== 'cuda' && backend !== 'rocm'
|
||||
? 'QuantMatMul is only available for CUDA and ROCm backends.'
|
||||
: 'Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing.'
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
|
|
@ -150,7 +176,7 @@ export const AdvancedTab = ({
|
|||
</Group>
|
||||
<Stack gap="md">
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={noavx2}
|
||||
|
|
@ -170,7 +196,7 @@ export const AdvancedTab = ({
|
|||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={failsafe}
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
import {
|
||||
Stack,
|
||||
Text,
|
||||
Group,
|
||||
TextInput,
|
||||
Button,
|
||||
Checkbox,
|
||||
Slider,
|
||||
} from '@mantine/core';
|
||||
import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
|
||||
import { File, Search } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { BackendSelector } from '@/screens/Launch/BackendSelector';
|
||||
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
|
||||
import { getInputValidationState } from '@/utils';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface GeneralTabProps {
|
||||
modelPath: string;
|
||||
|
|
@ -77,7 +70,7 @@ export const GeneralTab = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Stack gap="md">
|
||||
<BackendSelector
|
||||
backend={backend}
|
||||
onBackendChange={onBackendChange}
|
||||
|
|
@ -87,6 +80,10 @@ export const GeneralTab = ({
|
|||
failsafe={failsafe}
|
||||
onWarningsChange={onWarningsChange}
|
||||
onBackendsReady={onBackendsReady}
|
||||
gpuLayers={gpuLayers}
|
||||
autoGpuLayers={autoGpuLayers}
|
||||
onGpuLayersChange={onGpuLayersChange}
|
||||
onAutoGpuLayersChange={onAutoGpuLayersChange}
|
||||
/>
|
||||
|
||||
<div>
|
||||
|
|
@ -94,7 +91,7 @@ export const GeneralTab = ({
|
|||
Text Model File
|
||||
</Text>
|
||||
<Group gap="xs" align="flex-start">
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className={styles.flex1}>
|
||||
<TextInput
|
||||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||
value={modelPath}
|
||||
|
|
@ -126,51 +123,6 @@ export const GeneralTab = ({
|
|||
</Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="sm" fw={500}>
|
||||
GPU Layers
|
||||
</Text>
|
||||
<InfoTooltip label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance." />
|
||||
</Group>
|
||||
<Group gap="lg" align="center">
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
label="Auto"
|
||||
checked={autoGpuLayers}
|
||||
onChange={(event) =>
|
||||
onAutoGpuLayersChange(event.currentTarget.checked)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
<InfoTooltip label="Automatically try to allocate the GPU layers based on available VRAM." />
|
||||
</Group>
|
||||
<TextInput
|
||||
value={gpuLayers.toString()}
|
||||
onChange={(event) =>
|
||||
onGpuLayersChange(Number(event.target.value) || 0)
|
||||
}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
size="sm"
|
||||
w={80}
|
||||
disabled={autoGpuLayers}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Slider
|
||||
value={gpuLayers}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={onGpuLayersChange}
|
||||
disabled={autoGpuLayers}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
<Group gap="xs" align="center">
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Text, Group, Badge } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
interface BackendSelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
||||
label: string;
|
||||
devices?: string[];
|
||||
}
|
||||
|
||||
export const BackendSelectItem = forwardRef<
|
||||
HTMLDivElement,
|
||||
BackendSelectItemProps
|
||||
>(({ label, devices, ...others }, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" truncate>
|
||||
{label}
|
||||
</Text>
|
||||
{devices && devices.length > 0 && (
|
||||
<Group gap={4}>
|
||||
{devices.slice(0, 2).map((device, index) => (
|
||||
<Badge key={index} size="md" variant="light" color="blue">
|
||||
{device.length > 18 ? `${device.slice(0, 18)}...` : device}
|
||||
</Badge>
|
||||
))}
|
||||
{devices.length > 2 && (
|
||||
<Badge size="md" variant="light" color="gray">
|
||||
+{devices.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
));
|
||||
|
||||
BackendSelectItem.displayName = 'BackendSelectItem';
|
||||
275
src/components/screens/Launch/GeneralTab/BackendSelector.tsx
Normal file
275
src/components/screens/Launch/GeneralTab/BackendSelector.tsx
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import { Text, Group, Select, Checkbox, TextInput } from '@mantine/core';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
|
||||
|
||||
interface BackendSelectorProps {
|
||||
backend: string;
|
||||
onBackendChange: (backend: string) => void;
|
||||
gpuDevice?: number;
|
||||
onGpuDeviceChange?: (device: number) => void;
|
||||
noavx2?: boolean;
|
||||
failsafe?: boolean;
|
||||
onWarningsChange?: (
|
||||
warnings: Array<{ type: 'warning' | 'info'; message: string }>
|
||||
) => void;
|
||||
onBackendsReady?: () => void;
|
||||
gpuLayers?: number;
|
||||
autoGpuLayers?: boolean;
|
||||
onGpuLayersChange?: (layers: number) => void;
|
||||
onAutoGpuLayersChange?: (auto: boolean) => void;
|
||||
}
|
||||
|
||||
export const BackendSelector = ({
|
||||
backend,
|
||||
onBackendChange,
|
||||
gpuDevice = 0,
|
||||
onGpuDeviceChange,
|
||||
noavx2 = false,
|
||||
failsafe = false,
|
||||
onWarningsChange,
|
||||
onBackendsReady,
|
||||
gpuLayers = 0,
|
||||
autoGpuLayers = false,
|
||||
onGpuLayersChange,
|
||||
onAutoGpuLayersChange,
|
||||
}: BackendSelectorProps) => {
|
||||
const [availableBackends, setAvailableBackends] = useState<
|
||||
Array<{ value: string; label: string; devices?: string[] }>
|
||||
>([]);
|
||||
const [cpuCapabilities, setCpuCapabilities] = useState<{
|
||||
avx: boolean;
|
||||
avx2: boolean;
|
||||
} | null>(null);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) return;
|
||||
|
||||
let timeoutId: number;
|
||||
|
||||
const loadBackends = async () => {
|
||||
try {
|
||||
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentBinaryInfo(),
|
||||
window.electronAPI.kobold.detectCPU(),
|
||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||
]);
|
||||
|
||||
setCpuCapabilities({
|
||||
avx: cpuCapabilitiesResult.avx,
|
||||
avx2: cpuCapabilitiesResult.avx2,
|
||||
});
|
||||
|
||||
let backends: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
devices?: string[];
|
||||
}> = [];
|
||||
|
||||
if (currentBinaryInfo?.path) {
|
||||
backends = await window.electronAPI.kobold.getAvailableBackends(
|
||||
currentBinaryInfo.path,
|
||||
gpuCapabilities
|
||||
);
|
||||
|
||||
const cpuBackend = backends.find((b) => b.value === 'cpu');
|
||||
if (cpuBackend) {
|
||||
cpuBackend.devices = cpuCapabilitiesResult.devices;
|
||||
}
|
||||
}
|
||||
|
||||
setAvailableBackends(backends);
|
||||
hasInitialized.current = true;
|
||||
|
||||
if (backends.length > 0 && !backend) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
onBackendChange(backends[0].value);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
if (onBackendsReady) {
|
||||
window.setTimeout(() => {
|
||||
onBackendsReady();
|
||||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to detect available backends:',
|
||||
error as Error
|
||||
);
|
||||
setAvailableBackends([]);
|
||||
|
||||
if (onBackendsReady) {
|
||||
onBackendsReady();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadBackends();
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [backend, onBackendChange, onBackendsReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onWarningsChange) return;
|
||||
|
||||
if (backend !== 'cpu' || !cpuCapabilities) {
|
||||
onWarningsChange([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = [];
|
||||
|
||||
if (!cpuCapabilities.avx2 && !noavx2) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Your CPU does not support AVX2. Enable the "Disable AVX2" option to avoid crashes.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option to avoid crashes.',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
availableBackends.length > 0 &&
|
||||
availableBackends.some((b) => b.value === 'cpu')
|
||||
) {
|
||||
warnings.push({
|
||||
type: 'info',
|
||||
message:
|
||||
"Performance Note: LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA or AMD's ROCm backends for optimal performance.",
|
||||
});
|
||||
}
|
||||
|
||||
onWarningsChange(warnings);
|
||||
}, [
|
||||
backend,
|
||||
cpuCapabilities,
|
||||
noavx2,
|
||||
failsafe,
|
||||
availableBackends,
|
||||
onWarningsChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group justify="space-between" align="flex-start" mb="xs">
|
||||
<div style={{ flex: 1, marginRight: '2rem' }}>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Backend
|
||||
</Text>
|
||||
<InfoTooltip label="Select a backend to use. CUDA runs on NVIDIA GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
|
||||
</Group>
|
||||
<Select
|
||||
placeholder="Select backend"
|
||||
value={backend}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
onBackendChange(value);
|
||||
}
|
||||
}}
|
||||
data={availableBackends.map((b) => ({
|
||||
value: b.value,
|
||||
label: b.label,
|
||||
}))}
|
||||
disabled={availableBackends.length === 0}
|
||||
comboboxProps={{
|
||||
middlewares: {
|
||||
flip: false,
|
||||
},
|
||||
}}
|
||||
renderOption={({ option }) => {
|
||||
const backendData = availableBackends.find(
|
||||
(b) => b.value === option.value
|
||||
);
|
||||
return (
|
||||
<BackendSelectItem
|
||||
label={backendData?.label || option.label.split(' (')[0]}
|
||||
devices={backendData?.devices}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
const selectedBackend = availableBackends.find(
|
||||
(b) => b.value === backend
|
||||
);
|
||||
const isGpuBackend = backend === 'cuda' || backend === 'rocm';
|
||||
const hasMultipleDevices =
|
||||
selectedBackend?.devices && selectedBackend.devices.length > 1;
|
||||
|
||||
return (
|
||||
isGpuBackend &&
|
||||
onGpuDeviceChange &&
|
||||
hasMultipleDevices && (
|
||||
<Select
|
||||
label="GPU Device"
|
||||
placeholder="Select GPU device"
|
||||
value={gpuDevice.toString()}
|
||||
onChange={(value) =>
|
||||
value && onGpuDeviceChange(parseInt(value, 10))
|
||||
}
|
||||
data={selectedBackend.devices!.map((device, index) => ({
|
||||
value: index.toString(),
|
||||
label: `GPU ${index}: ${device}`,
|
||||
}))}
|
||||
mt="xs"
|
||||
/>
|
||||
)
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{onGpuLayersChange && onAutoGpuLayersChange && (
|
||||
<div style={{ flex: 1 }}>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
GPU Layers
|
||||
</Text>
|
||||
<InfoTooltip label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance." />
|
||||
</Group>
|
||||
<Group gap="lg" align="center">
|
||||
<TextInput
|
||||
value={gpuLayers.toString()}
|
||||
onChange={(event) =>
|
||||
onGpuLayersChange(Number(event.target.value) || 0)
|
||||
}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
size="sm"
|
||||
w={80}
|
||||
disabled={autoGpuLayers}
|
||||
/>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
label="Auto"
|
||||
checked={autoGpuLayers}
|
||||
onChange={(event) =>
|
||||
onAutoGpuLayersChange(event.currentTarget.checked)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
<InfoTooltip label="Automatically try to allocate the GPU layers based on available VRAM." />
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
157
src/components/screens/Launch/GeneralTab/index.tsx
Normal file
157
src/components/screens/Launch/GeneralTab/index.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
|
||||
import { File, Search } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
|
||||
import { getInputValidationState } from '@/utils';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface GeneralTabProps {
|
||||
modelPath: string;
|
||||
gpuLayers: number;
|
||||
autoGpuLayers: boolean;
|
||||
contextSize: number;
|
||||
backend: string;
|
||||
gpuDevice?: number;
|
||||
noavx2: boolean;
|
||||
failsafe: boolean;
|
||||
onModelPathChange: (path: string) => void;
|
||||
onSelectModelFile: () => void;
|
||||
onGpuLayersChange: (layers: number) => void;
|
||||
onAutoGpuLayersChange: (auto: boolean) => void;
|
||||
onContextSizeChange: (size: number) => void;
|
||||
onBackendChange: (backend: string) => void;
|
||||
onGpuDeviceChange?: (device: number) => void;
|
||||
onWarningsChange?: (
|
||||
warnings: Array<{ type: 'warning' | 'info'; message: string }>
|
||||
) => void;
|
||||
onBackendsReady?: () => void;
|
||||
}
|
||||
|
||||
export const GeneralTab = ({
|
||||
modelPath,
|
||||
gpuLayers,
|
||||
autoGpuLayers,
|
||||
contextSize,
|
||||
backend,
|
||||
gpuDevice,
|
||||
noavx2,
|
||||
failsafe,
|
||||
onModelPathChange,
|
||||
onSelectModelFile,
|
||||
onGpuLayersChange,
|
||||
onAutoGpuLayersChange,
|
||||
onContextSizeChange,
|
||||
onBackendChange,
|
||||
onGpuDeviceChange,
|
||||
onWarningsChange,
|
||||
onBackendsReady,
|
||||
}: GeneralTabProps) => {
|
||||
const validationState = getInputValidationState(modelPath);
|
||||
|
||||
const getInputColor = () => {
|
||||
switch (validationState) {
|
||||
case 'valid':
|
||||
return 'green';
|
||||
case 'invalid':
|
||||
return 'red';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getHelperText = () => {
|
||||
if (!modelPath.trim()) return undefined;
|
||||
|
||||
if (validationState === 'invalid') {
|
||||
return 'Enter a valid URL or file path to the .gguf';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<BackendSelector
|
||||
backend={backend}
|
||||
onBackendChange={onBackendChange}
|
||||
gpuDevice={gpuDevice}
|
||||
onGpuDeviceChange={onGpuDeviceChange}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
onWarningsChange={onWarningsChange}
|
||||
onBackendsReady={onBackendsReady}
|
||||
gpuLayers={gpuLayers}
|
||||
autoGpuLayers={autoGpuLayers}
|
||||
onGpuLayersChange={onGpuLayersChange}
|
||||
onAutoGpuLayersChange={onAutoGpuLayersChange}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">
|
||||
Text Model File
|
||||
</Text>
|
||||
<Group gap="xs" align="flex-start">
|
||||
<div className={styles.flex1}>
|
||||
<TextInput
|
||||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||
value={modelPath}
|
||||
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
||||
color={getInputColor()}
|
||||
error={
|
||||
validationState === 'invalid' ? getHelperText() : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onSelectModelFile}
|
||||
variant="light"
|
||||
leftSection={<File size={16} />}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.electronAPI.app.openExternal(
|
||||
'https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending'
|
||||
);
|
||||
}}
|
||||
variant="outline"
|
||||
leftSection={<Search size={16} />}
|
||||
>
|
||||
Search HF
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="sm" fw={500}>
|
||||
Context Size
|
||||
</Text>
|
||||
<InfoTooltip label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory." />
|
||||
</Group>
|
||||
<TextInput
|
||||
value={contextSize?.toString() || ''}
|
||||
onChange={(event) =>
|
||||
onContextSizeChange(Number(event.target.value) || 256)
|
||||
}
|
||||
type="number"
|
||||
min={256}
|
||||
max={131072}
|
||||
step={256}
|
||||
size="sm"
|
||||
w={100}
|
||||
/>
|
||||
</Group>
|
||||
<Slider
|
||||
value={contextSize}
|
||||
min={256}
|
||||
max={131072}
|
||||
step={1}
|
||||
onChange={onContextSizeChange}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||
import { File, Search } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface ImageGenerationTabProps {
|
||||
sdmodel: string;
|
||||
|
|
@ -78,7 +79,7 @@ const ModelField = ({
|
|||
{tooltip && <InfoTooltip label={tooltip} />}
|
||||
</Group>
|
||||
<Group gap="xs" align="flex-start">
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className={styles.flex1}>
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
|
|
@ -139,7 +140,7 @@ export const ImageGenerationTab = ({
|
|||
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
interface NetworkTabProps {
|
||||
port: number | undefined;
|
||||
|
|
@ -42,7 +43,7 @@ export const NetworkTab = ({
|
|||
}, [port]);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Stack gap="md">
|
||||
<Group gap="lg" align="flex-start">
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
|
|
@ -100,7 +101,7 @@ export const NetworkTab = ({
|
|||
<div>
|
||||
<Stack gap="md">
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={multiuser}
|
||||
|
|
@ -113,7 +114,7 @@ export const NetworkTab = ({
|
|||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={multiplayer}
|
||||
|
|
@ -128,7 +129,7 @@ export const NetworkTab = ({
|
|||
</Group>
|
||||
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={remotetunnel}
|
||||
|
|
@ -141,7 +142,7 @@ export const NetworkTab = ({
|
|||
</Group>
|
||||
</div>
|
||||
|
||||
<div style={{ minWidth: '200px' }}>
|
||||
<div className={styles.minWidth200}>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={nocertify}
|
||||
503
src/components/screens/Launch/index.tsx
Normal file
503
src/components/screens/Launch/index.tsx
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useTrackedConfigHandlers } from '@/hooks/useTrackedConfigHandlers';
|
||||
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
|
||||
import { useWarnings } from '@/hooks/useWarnings';
|
||||
import { GeneralTab } from '@/components/screens/Launch/GeneralTab';
|
||||
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
|
||||
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
|
||||
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
|
||||
import { WarningDisplay } from '@/components/WarningDisplay';
|
||||
import { ConfigFileManager } from '@/components/ConfigFileManager';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface LaunchScreenProps {
|
||||
onLaunch: () => void;
|
||||
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||
}
|
||||
|
||||
export const LaunchScreen = ({
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
}: LaunchScreenProps) => {
|
||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [, setInstallDir] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string | null>('general');
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const [warnings, setWarnings] = useState<
|
||||
Array<{ type: 'warning' | 'info'; message: string }>
|
||||
>([]);
|
||||
|
||||
const {
|
||||
gpuLayers,
|
||||
autoGpuLayers,
|
||||
contextSize,
|
||||
modelPath,
|
||||
additionalArguments,
|
||||
port,
|
||||
host,
|
||||
multiuser,
|
||||
multiplayer,
|
||||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
noshift,
|
||||
flashattention,
|
||||
noavx2,
|
||||
failsafe,
|
||||
lowvram,
|
||||
quantmatmul,
|
||||
backend,
|
||||
gpuDevice,
|
||||
sdmodel,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
sdlora,
|
||||
parseAndApplyConfigFile,
|
||||
loadSavedSettings,
|
||||
loadConfigFromFile,
|
||||
handleSelectModelFile,
|
||||
handleGpuDeviceChange,
|
||||
handleSelectSdmodelFile,
|
||||
handleSelectSdt5xxlFile,
|
||||
handleSelectSdcliplFile,
|
||||
handleSelectSdclipgFile,
|
||||
handleSelectSdphotomakerFile,
|
||||
handleSelectSdvaeFile,
|
||||
handleSelectSdloraFile,
|
||||
handleApplyPreset,
|
||||
handleModelPathChange,
|
||||
handleGpuLayersChange,
|
||||
handleAutoGpuLayersChange,
|
||||
handleContextSizeChangeWithStep,
|
||||
handleAdditionalArgumentsChange,
|
||||
handlePortChange,
|
||||
handleHostChange,
|
||||
handleMultiuserChange,
|
||||
handleMultiplayerChange,
|
||||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleLowvramChange,
|
||||
handleQuantmatmulChange,
|
||||
handleBackendChange,
|
||||
handleSdmodelChange,
|
||||
handleSdt5xxlChange,
|
||||
handleSdcliplChange,
|
||||
handleSdclipgChange,
|
||||
handleSdphotomakerChange,
|
||||
handleSdvaeChange,
|
||||
handleSdloraChange,
|
||||
} = useLaunchConfig();
|
||||
|
||||
const trackedHandlers = useTrackedConfigHandlers({
|
||||
setHasUnsavedChanges,
|
||||
handlers: {
|
||||
handleModelPathChange,
|
||||
handleGpuLayersChange,
|
||||
handleAutoGpuLayersChange,
|
||||
handleContextSizeChangeWithStep,
|
||||
handleAdditionalArgumentsChange,
|
||||
handlePortChange,
|
||||
handleHostChange,
|
||||
handleMultiuserChange,
|
||||
handleMultiplayerChange,
|
||||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleLowvramChange,
|
||||
handleQuantmatmulChange,
|
||||
handleBackendChange,
|
||||
handleSdmodelChange,
|
||||
handleSdt5xxlChange,
|
||||
handleSdcliplChange,
|
||||
handleSdclipgChange,
|
||||
handleSdphotomakerChange,
|
||||
handleSdvaeChange,
|
||||
handleSdloraChange,
|
||||
},
|
||||
});
|
||||
|
||||
const { isLaunching, handleLaunch } = useLaunchLogic({
|
||||
configFiles,
|
||||
selectedFile,
|
||||
modelPath,
|
||||
sdmodel,
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
});
|
||||
|
||||
const combinedWarnings = useWarnings({ modelPath, sdmodel, warnings });
|
||||
|
||||
const loadConfigFiles = useCallback(async () => {
|
||||
const [files, currentDir, savedConfig] = await Promise.all([
|
||||
window.electronAPI.kobold.getConfigFiles(),
|
||||
window.electronAPI.kobold.getCurrentInstallDir(),
|
||||
window.electronAPI.kobold.getSelectedConfig(),
|
||||
]);
|
||||
|
||||
setConfigFiles(files);
|
||||
setInstallDir(currentDir);
|
||||
|
||||
if (savedConfig && files.some((f) => f.name === savedConfig)) {
|
||||
setSelectedFile(savedConfig);
|
||||
} else if (files.length > 0 && !selectedFile) {
|
||||
setSelectedFile(files[0].name);
|
||||
}
|
||||
|
||||
await loadSavedSettings();
|
||||
|
||||
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
|
||||
if (currentSelectedFile && !selectedFile) {
|
||||
setSelectedFile(currentSelectedFile);
|
||||
}
|
||||
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
|
||||
|
||||
const handleFileSelection = async (fileName: string) => {
|
||||
setSelectedFile(fileName);
|
||||
await window.electronAPI.kobold.setSelectedConfig(fileName);
|
||||
|
||||
const selectedConfig = configFiles.find((f) => f.name === fileName);
|
||||
if (selectedConfig) {
|
||||
await parseAndApplyConfigFile(selectedConfig.path);
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
};
|
||||
|
||||
const buildConfigData = () => ({
|
||||
gpulayers: gpuLayers,
|
||||
contextsize: contextSize,
|
||||
model_param: modelPath,
|
||||
port,
|
||||
host,
|
||||
multiuser: multiuser ? 1 : 0,
|
||||
multiplayer,
|
||||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
noshift,
|
||||
flashattention,
|
||||
noavx2,
|
||||
failsafe,
|
||||
usecuda: backend === 'cuda' || backend === 'rocm',
|
||||
usevulkan: backend === 'vulkan',
|
||||
useclblast: backend === 'clblast',
|
||||
sdmodel,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
});
|
||||
|
||||
const handleCreateNewConfig = async (configName: string) => {
|
||||
try {
|
||||
const success = await window.electronAPI.kobold.saveConfigFile(
|
||||
configName,
|
||||
buildConfigData()
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await loadConfigFiles();
|
||||
const newFileName = `${configName}.kcpps`;
|
||||
setSelectedFile(newFileName);
|
||||
await window.electronAPI.kobold.setSelectedConfig(newFileName);
|
||||
setHasUnsavedChanges(false);
|
||||
} else {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to create new configuration',
|
||||
new Error('Save operation failed')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to create new configuration:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!selectedFile) {
|
||||
window.electronAPI.logs.logError(
|
||||
'No configuration file selected for saving',
|
||||
new Error('Selected file is null')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configName = selectedFile.replace('.kcpps', '');
|
||||
|
||||
const success = await window.electronAPI.kobold.saveConfigFile(
|
||||
configName,
|
||||
buildConfigData()
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setHasUnsavedChanges(false);
|
||||
} else {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to save configuration',
|
||||
new Error('Save operation failed')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to save configuration:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfigFiles();
|
||||
|
||||
const handleInstallDirChange = () => {
|
||||
void loadConfigFiles();
|
||||
};
|
||||
|
||||
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
|
||||
handleInstallDirChange
|
||||
);
|
||||
|
||||
return cleanup;
|
||||
}, [loadConfigFiles]);
|
||||
|
||||
const handleLaunchClick = () => {
|
||||
handleLaunch({
|
||||
autoGpuLayers,
|
||||
gpuLayers,
|
||||
contextSize,
|
||||
port: port ?? 5001,
|
||||
host,
|
||||
multiuser,
|
||||
multiplayer,
|
||||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
noshift,
|
||||
flashattention,
|
||||
backend,
|
||||
lowvram,
|
||||
gpuDevice,
|
||||
quantmatmul,
|
||||
additionalArguments,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
sdlora,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="sm">
|
||||
<Stack gap="md">
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<ConfigFileManager
|
||||
configFiles={configFiles}
|
||||
selectedFile={selectedFile}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
onFileSelection={handleFileSelection}
|
||||
onCreateNewConfig={handleCreateNewConfig}
|
||||
onSaveConfig={handleSaveConfig}
|
||||
onLoadConfigFiles={loadConfigFiles}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||
<Tabs.Tab value="image">Image Generation</Tabs.Tab>
|
||||
<Tabs.Tab value="network">Network</Tabs.Tab>
|
||||
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<div>
|
||||
<Tabs.Panel value="general" pt="md">
|
||||
<GeneralTab
|
||||
modelPath={modelPath}
|
||||
gpuLayers={gpuLayers}
|
||||
autoGpuLayers={autoGpuLayers}
|
||||
contextSize={contextSize}
|
||||
backend={backend}
|
||||
gpuDevice={gpuDevice}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
onModelPathChange={
|
||||
trackedHandlers.handleModelPathChangeWithTracking
|
||||
}
|
||||
onSelectModelFile={handleSelectModelFile}
|
||||
onGpuLayersChange={
|
||||
trackedHandlers.handleGpuLayersChangeWithTracking
|
||||
}
|
||||
onAutoGpuLayersChange={
|
||||
trackedHandlers.handleAutoGpuLayersChangeWithTracking
|
||||
}
|
||||
onContextSizeChange={
|
||||
trackedHandlers.handleContextSizeChangeWithTracking
|
||||
}
|
||||
onBackendChange={
|
||||
trackedHandlers.handleBackendChangeWithTracking
|
||||
}
|
||||
onGpuDeviceChange={handleGpuDeviceChange}
|
||||
onWarningsChange={setWarnings}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="advanced" pt="md">
|
||||
<AdvancedTab
|
||||
additionalArguments={additionalArguments}
|
||||
noshift={noshift}
|
||||
flashattention={flashattention}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
lowvram={lowvram}
|
||||
quantmatmul={quantmatmul}
|
||||
backend={backend}
|
||||
onAdditionalArgumentsChange={
|
||||
trackedHandlers.handleAdditionalArgumentsChangeWithTracking
|
||||
}
|
||||
onNoshiftChange={
|
||||
trackedHandlers.handleNoshiftChangeWithTracking
|
||||
}
|
||||
onFlashattentionChange={
|
||||
trackedHandlers.handleFlashattentionChangeWithTracking
|
||||
}
|
||||
onNoavx2Change={
|
||||
trackedHandlers.handleNoavx2ChangeWithTracking
|
||||
}
|
||||
onFailsafeChange={
|
||||
trackedHandlers.handleFailsafeChangeWithTracking
|
||||
}
|
||||
onLowvramChange={
|
||||
trackedHandlers.handleLowvramChangeWithTracking
|
||||
}
|
||||
onQuantmatmulChange={
|
||||
trackedHandlers.handleQuantmatmulChangeWithTracking
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="network" pt="md">
|
||||
<NetworkTab
|
||||
port={port}
|
||||
host={host}
|
||||
multiuser={multiuser}
|
||||
multiplayer={multiplayer}
|
||||
remotetunnel={remotetunnel}
|
||||
nocertify={nocertify}
|
||||
websearch={websearch}
|
||||
onPortChange={trackedHandlers.handlePortChangeWithTracking}
|
||||
onHostChange={trackedHandlers.handleHostChangeWithTracking}
|
||||
onMultiuserChange={
|
||||
trackedHandlers.handleMultiuserChangeWithTracking
|
||||
}
|
||||
onMultiplayerChange={
|
||||
trackedHandlers.handleMultiplayerChangeWithTracking
|
||||
}
|
||||
onRemotetunnelChange={
|
||||
trackedHandlers.handleRemotetunnelChangeWithTracking
|
||||
}
|
||||
onNocertifyChange={
|
||||
trackedHandlers.handleNocertifyChangeWithTracking
|
||||
}
|
||||
onWebsearchChange={
|
||||
trackedHandlers.handleWebsearchChangeWithTracking
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="image" pt="md">
|
||||
<ImageGenerationTab
|
||||
sdmodel={sdmodel}
|
||||
sdt5xxl={sdt5xxl}
|
||||
sdclipl={sdclipl}
|
||||
sdclipg={sdclipg}
|
||||
sdphotomaker={sdphotomaker}
|
||||
sdvae={sdvae}
|
||||
sdlora={sdlora}
|
||||
onSdmodelChange={
|
||||
trackedHandlers.handleSdmodelChangeWithTracking
|
||||
}
|
||||
onSelectSdmodelFile={handleSelectSdmodelFile}
|
||||
onSdt5xxlChange={
|
||||
trackedHandlers.handleSdt5xxlChangeWithTracking
|
||||
}
|
||||
onSelectSdt5xxlFile={handleSelectSdt5xxlFile}
|
||||
onSdcliplChange={
|
||||
trackedHandlers.handleSdcliplChangeWithTracking
|
||||
}
|
||||
onSelectSdcliplFile={handleSelectSdcliplFile}
|
||||
onSdclipgChange={
|
||||
trackedHandlers.handleSdclipgChangeWithTracking
|
||||
}
|
||||
onSelectSdclipgFile={handleSelectSdclipgFile}
|
||||
onSdphotomakerChange={
|
||||
trackedHandlers.handleSdphotomakerChangeWithTracking
|
||||
}
|
||||
onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
|
||||
onSdvaeChange={
|
||||
trackedHandlers.handleSdvaeChangeWithTracking
|
||||
}
|
||||
onSelectSdvaeFile={handleSelectSdvaeFile}
|
||||
onSdloraChange={
|
||||
trackedHandlers.handleSdloraChangeWithTracking
|
||||
}
|
||||
onSelectSdloraFile={handleSelectSdloraFile}
|
||||
onApplyPreset={handleApplyPreset}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
<Group justify="flex-end" pt="md">
|
||||
<WarningDisplay warnings={combinedWarnings}>
|
||||
<Button
|
||||
radius="md"
|
||||
disabled={(!modelPath && !sdmodel) || isLaunching}
|
||||
onClick={handleLaunchClick}
|
||||
size="lg"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
padding: '12px 28px',
|
||||
minWidth: '120px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
</WarningDisplay>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
|
||||
import { Folder, FolderOpen } from 'lucide-react';
|
||||
import styles from '@/styles/layout.module.css';
|
||||
|
||||
export const GeneralTab = () => {
|
||||
const [installDir, setInstallDir] = useState<string>('');
|
||||
|
|
@ -51,7 +52,7 @@ export const GeneralTab = () => {
|
|||
value={installDir}
|
||||
readOnly
|
||||
placeholder="Default installation directory"
|
||||
style={{ flex: 1 }}
|
||||
className={styles.flex1}
|
||||
leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />}
|
||||
/>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export const SettingsModal = ({
|
|||
</Tabs.Panel>
|
||||
|
||||
{showVersionsTab && (
|
||||
<Tabs.Panel value="versions">
|
||||
<Tabs.Panel value="versions" style={{ overflow: 'visible' }}>
|
||||
<VersionsTab />
|
||||
</Tabs.Panel>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@ import {
|
|||
Loader,
|
||||
rem,
|
||||
Center,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { RotateCcw, ExternalLink } from 'lucide-react';
|
||||
import { DownloadCard } from '@/components/DownloadCard';
|
||||
import {
|
||||
getAssetDescription,
|
||||
sortAssetsByRecommendation,
|
||||
isAssetRecommended,
|
||||
} from '@/utils/assets';
|
||||
import { getDisplayNameFromPath } from '@/utils';
|
||||
import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import type { InstalledVersion } from '@/types/electron';
|
||||
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||
|
||||
interface VersionInfo {
|
||||
name: string;
|
||||
|
|
@ -50,6 +51,9 @@ export const VersionsTab = () => {
|
|||
null
|
||||
);
|
||||
const [loadingInstalled, setLoadingInstalled] = useState(true);
|
||||
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const loadInstalledVersions = useCallback(async () => {
|
||||
setLoadingInstalled(true);
|
||||
|
|
@ -95,9 +99,23 @@ export const VersionsTab = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const loadLatestRelease = useCallback(async () => {
|
||||
try {
|
||||
const release =
|
||||
await window.electronAPI.kobold.getLatestReleaseWithStatus();
|
||||
setLatestRelease(release);
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to load latest release:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadInstalledVersions();
|
||||
}, [loadInstalledVersions]);
|
||||
loadLatestRelease();
|
||||
}, [loadInstalledVersions, loadLatestRelease]);
|
||||
|
||||
const getAllVersions = (): VersionInfo[] => {
|
||||
const versions: VersionInfo[] = [];
|
||||
|
|
@ -105,7 +123,10 @@ export const VersionsTab = () => {
|
|||
availableDownloads.forEach((download) => {
|
||||
const installedVersion = installedVersions.find((v) => {
|
||||
const displayName = getDisplayNameFromPath(v);
|
||||
return displayName === download.name;
|
||||
const downloadBaseName = download.name
|
||||
.replace(/\.(tar\.gz|zip|exe)$/i, '')
|
||||
.replace(/\.packed$/, '');
|
||||
return displayName === downloadBaseName;
|
||||
});
|
||||
|
||||
const isCurrent = Boolean(
|
||||
|
|
@ -217,15 +238,41 @@ export const VersionsTab = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg" h="100%">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={500}>Available Versions</Text>
|
||||
<>
|
||||
<Group justify="space-between" align="center" mb="lg">
|
||||
<div>
|
||||
<Text fw={500}>Available Versions</Text>
|
||||
{latestRelease && (
|
||||
<Group gap="xs" mt={4}>
|
||||
<Text size="sm" c="dimmed">
|
||||
Latest release: {latestRelease.release.tag_name}
|
||||
</Text>
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.electronAPI.app.openExternal(
|
||||
latestRelease.release.html_url
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
c="blue"
|
||||
>
|
||||
<Group gap={4} align="center">
|
||||
<span>Release notes</span>
|
||||
<ExternalLink size={12} />
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
loadInstalledVersions();
|
||||
loadRemoteVersions();
|
||||
loadLatestRelease();
|
||||
}}
|
||||
leftSection={
|
||||
<RotateCcw style={{ width: rem(14), height: rem(14) }} />
|
||||
|
|
@ -235,17 +282,21 @@ export const VersionsTab = () => {
|
|||
</Button>
|
||||
</Group>
|
||||
|
||||
<Stack gap="xs">
|
||||
{getAllVersions().map((version, index) => {
|
||||
const isDownloading = downloading === version.name;
|
||||
{getAllVersions().map((version, index) => {
|
||||
const isDownloading = downloading === version.name;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
key={`${version.name}-${version.version}-${index}`}
|
||||
style={{ paddingBottom: '8px' }}
|
||||
>
|
||||
<DownloadCard
|
||||
key={`${version.name}-${version.version}-${index}`}
|
||||
name={version.name}
|
||||
size={
|
||||
version.size
|
||||
? `${(version.size / 1024 / 1024).toFixed(1)} MB`
|
||||
? version.isROCm
|
||||
? `~${formatFileSizeInMB(version.size)}`
|
||||
: formatFileSizeInMB(version.size)
|
||||
: ''
|
||||
}
|
||||
version={version.version}
|
||||
|
|
@ -273,17 +324,17 @@ export const VersionsTab = () => {
|
|||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{getAllVersions().length === 0 && (
|
||||
<Card withBorder radius="md" padding="md">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
No versions found
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
{getAllVersions().length === 0 && (
|
||||
<Card withBorder radius="md" padding="md">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
No versions found
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export const DEFAULT_CONTEXT_SIZE = 4096;
|
|||
export const DEFAULT_MODEL_URL =
|
||||
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true';
|
||||
|
||||
export const DEFAULT_HOST = 'localhost';
|
||||
export const DEFAULT_HOST = '';
|
||||
|
||||
export const DEFAULT_VOLUME = 0.5;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ export const APP_NAME = 'friendly-kobold';
|
|||
|
||||
export const CONFIG_FILE_NAME = 'config.json';
|
||||
|
||||
export const UI = {
|
||||
HEADER_HEIGHT: 60,
|
||||
} as const;
|
||||
|
||||
export const { HEADER_HEIGHT } = UI;
|
||||
|
||||
export * from './defaults';
|
||||
|
||||
export const GITHUB_API = {
|
||||
|
|
@ -33,5 +39,5 @@ export const KOBOLDAI_URLS = {
|
|||
export const ROCM = {
|
||||
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
|
||||
SIZE_BYTES: 1024 * 1024 * 1024,
|
||||
SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
DEFAULT_CONTEXT_SIZE,
|
||||
DEFAULT_MODEL_URL,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_FLUX_MODELS,
|
||||
} from '@/constants';
|
||||
import { isNotNullish } from '@/utils';
|
||||
|
||||
|
|
@ -242,7 +241,7 @@ export const useLaunchConfig = () => {
|
|||
setSdclipl('');
|
||||
setSdclipg('');
|
||||
setSdphotomaker('');
|
||||
setSdvae(DEFAULT_FLUX_MODELS.VAE);
|
||||
setSdvae('');
|
||||
}, []);
|
||||
|
||||
const loadConfigFromFile = useCallback(
|
||||
|
|
|
|||
223
src/hooks/useLaunchLogic.ts
Normal file
223
src/hooks/useLaunchLogic.ts
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface UseLaunchLogicProps {
|
||||
configFiles: ConfigFile[];
|
||||
selectedFile: string | null;
|
||||
modelPath: string;
|
||||
sdmodel: string;
|
||||
onLaunch: () => void;
|
||||
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||
}
|
||||
|
||||
interface LaunchArgs {
|
||||
autoGpuLayers: boolean;
|
||||
gpuLayers: number;
|
||||
contextSize: number;
|
||||
port: number;
|
||||
host: string;
|
||||
multiuser: boolean;
|
||||
multiplayer: boolean;
|
||||
remotetunnel: boolean;
|
||||
nocertify: boolean;
|
||||
websearch: boolean;
|
||||
noshift: boolean;
|
||||
flashattention: boolean;
|
||||
backend: string;
|
||||
lowvram: boolean;
|
||||
gpuDevice: number;
|
||||
quantmatmul: boolean;
|
||||
additionalArguments: string;
|
||||
sdt5xxl: string;
|
||||
sdclipl: string;
|
||||
sdclipg: string;
|
||||
sdphotomaker: string;
|
||||
sdvae: string;
|
||||
sdlora: string;
|
||||
}
|
||||
|
||||
const buildModelArgs = (
|
||||
isImageMode: boolean,
|
||||
isTextMode: boolean,
|
||||
modelPath: string,
|
||||
sdmodel: string,
|
||||
launchArgs: LaunchArgs
|
||||
): string[] => {
|
||||
const args: string[] = [];
|
||||
|
||||
if (isImageMode && isTextMode) {
|
||||
args.push('--sdmodel', sdmodel);
|
||||
} else if (isImageMode) {
|
||||
args.push('--sdmodel', sdmodel);
|
||||
|
||||
const imageModels = [
|
||||
['--sdt5xxl', launchArgs.sdt5xxl],
|
||||
['--sdclipl', launchArgs.sdclipl],
|
||||
['--sdclipg', launchArgs.sdclipg],
|
||||
['--sdphotomaker', launchArgs.sdphotomaker],
|
||||
['--sdvae', launchArgs.sdvae],
|
||||
['--sdlora', launchArgs.sdlora],
|
||||
];
|
||||
|
||||
imageModels.forEach(([flag, value]) => {
|
||||
if (value.trim()) {
|
||||
args.push(flag, value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
args.push('--model', modelPath);
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
const buildConfigArgs = (launchArgs: LaunchArgs): string[] => {
|
||||
const args: string[] = [];
|
||||
|
||||
if (launchArgs.autoGpuLayers) {
|
||||
args.push('--gpulayers', '-1');
|
||||
} else if (launchArgs.gpuLayers > 0) {
|
||||
args.push('--gpulayers', launchArgs.gpuLayers.toString());
|
||||
}
|
||||
|
||||
if (launchArgs.contextSize) {
|
||||
args.push('--contextsize', launchArgs.contextSize.toString());
|
||||
}
|
||||
|
||||
if (launchArgs.port) {
|
||||
args.push('--port', launchArgs.port.toString());
|
||||
}
|
||||
|
||||
if (launchArgs.host !== 'localhost' && launchArgs.host) {
|
||||
args.push('--host', launchArgs.host);
|
||||
}
|
||||
|
||||
const flagMappings: Array<[boolean, string, string?]> = [
|
||||
[launchArgs.multiuser, '--multiuser', '1'],
|
||||
[launchArgs.multiplayer, '--multiplayer'],
|
||||
[launchArgs.remotetunnel, '--remotetunnel'],
|
||||
[launchArgs.nocertify, '--nocertify'],
|
||||
[launchArgs.websearch, '--websearch'],
|
||||
[launchArgs.noshift, '--noshift'],
|
||||
[launchArgs.flashattention, '--flashattention'],
|
||||
];
|
||||
|
||||
flagMappings.forEach(([condition, flag, value]) => {
|
||||
if (condition) {
|
||||
args.push(flag);
|
||||
if (value) args.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
const buildBackendArgs = (launchArgs: LaunchArgs): string[] => {
|
||||
const args: string[] = [];
|
||||
|
||||
if (launchArgs.backend && launchArgs.backend !== 'cpu') {
|
||||
if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') {
|
||||
const cudaArgs = ['--usecuda'];
|
||||
cudaArgs.push(launchArgs.lowvram ? 'lowvram' : 'normal');
|
||||
cudaArgs.push(launchArgs.gpuDevice.toString());
|
||||
cudaArgs.push(launchArgs.quantmatmul ? 'mmq' : 'nommq');
|
||||
args.push(...cudaArgs);
|
||||
} else if (launchArgs.backend === 'vulkan') {
|
||||
args.push('--usevulkan');
|
||||
} else if (launchArgs.backend === 'clblast') {
|
||||
args.push('--useclblast');
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
export const useLaunchLogic = ({
|
||||
configFiles,
|
||||
selectedFile,
|
||||
modelPath,
|
||||
sdmodel,
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
}: UseLaunchLogicProps) => {
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
|
||||
const handleLaunch = useCallback(
|
||||
async (launchArgs: LaunchArgs) => {
|
||||
const isImageMode = sdmodel.trim() !== '';
|
||||
const isTextMode = modelPath.trim() !== '';
|
||||
|
||||
if (isLaunching || (!isImageMode && !isTextMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLaunching(true);
|
||||
|
||||
try {
|
||||
const selectedConfig = selectedFile
|
||||
? configFiles.find((f) => f.name === selectedFile)
|
||||
: null;
|
||||
|
||||
const args: string[] = [
|
||||
...buildModelArgs(
|
||||
isImageMode,
|
||||
isTextMode,
|
||||
modelPath,
|
||||
sdmodel,
|
||||
launchArgs
|
||||
),
|
||||
...buildConfigArgs(launchArgs),
|
||||
...buildBackendArgs(launchArgs),
|
||||
];
|
||||
|
||||
if (launchArgs.additionalArguments.trim()) {
|
||||
const additionalArgs = launchArgs.additionalArguments
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
args.push(...additionalArgs);
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.kobold.launchKoboldCpp(
|
||||
args,
|
||||
selectedConfig?.path
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
if (onLaunchModeChange) {
|
||||
onLaunchModeChange(isImageMode);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
onLaunch();
|
||||
}, 100);
|
||||
} else {
|
||||
window.electronAPI.logs.logError(
|
||||
'Launch failed:',
|
||||
new Error(result.error)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Error launching KoboldCpp:',
|
||||
error as Error
|
||||
);
|
||||
} finally {
|
||||
setIsLaunching(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
configFiles,
|
||||
selectedFile,
|
||||
modelPath,
|
||||
sdmodel,
|
||||
isLaunching,
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
isLaunching,
|
||||
handleLaunch,
|
||||
};
|
||||
};
|
||||
176
src/hooks/useTrackedConfigHandlers.ts
Normal file
176
src/hooks/useTrackedConfigHandlers.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { useChangeTracker } from '@/hooks/useChangeTracker';
|
||||
|
||||
interface TrackedConfigHandlersProps {
|
||||
setHasUnsavedChanges: (value: boolean) => void;
|
||||
handlers: {
|
||||
handleModelPathChange: (path: string) => void;
|
||||
handleGpuLayersChange: (layers: number) => void;
|
||||
handleAutoGpuLayersChange: (auto: boolean) => void;
|
||||
handleContextSizeChangeWithStep: (size: number) => void;
|
||||
handleAdditionalArgumentsChange: (args: string) => void;
|
||||
handlePortChange: (port: number | undefined) => void;
|
||||
handleHostChange: (host: string) => void;
|
||||
handleMultiuserChange: (enabled: boolean) => void;
|
||||
handleMultiplayerChange: (enabled: boolean) => void;
|
||||
handleRemotetunnelChange: (enabled: boolean) => void;
|
||||
handleNocertifyChange: (enabled: boolean) => void;
|
||||
handleWebsearchChange: (enabled: boolean) => void;
|
||||
handleNoshiftChange: (enabled: boolean) => void;
|
||||
handleFlashattentionChange: (enabled: boolean) => void;
|
||||
handleNoavx2Change: (enabled: boolean) => void;
|
||||
handleFailsafeChange: (enabled: boolean) => void;
|
||||
handleLowvramChange: (enabled: boolean) => void;
|
||||
handleQuantmatmulChange: (enabled: boolean) => void;
|
||||
handleBackendChange: (backend: string) => void;
|
||||
handleSdmodelChange: (path: string) => void;
|
||||
handleSdt5xxlChange: (path: string) => void;
|
||||
handleSdcliplChange: (path: string) => void;
|
||||
handleSdclipgChange: (path: string) => void;
|
||||
handleSdphotomakerChange: (path: string) => void;
|
||||
handleSdvaeChange: (path: string) => void;
|
||||
handleSdloraChange: (path: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const useTrackedConfigHandlers = ({
|
||||
setHasUnsavedChanges,
|
||||
handlers,
|
||||
}: TrackedConfigHandlersProps) => {
|
||||
const createChangeTracker = useChangeTracker;
|
||||
|
||||
const {
|
||||
handleModelPathChange,
|
||||
handleGpuLayersChange,
|
||||
handleAutoGpuLayersChange,
|
||||
handleContextSizeChangeWithStep,
|
||||
handleAdditionalArgumentsChange,
|
||||
handlePortChange,
|
||||
handleHostChange,
|
||||
handleMultiuserChange,
|
||||
handleMultiplayerChange,
|
||||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleLowvramChange,
|
||||
handleQuantmatmulChange,
|
||||
handleBackendChange,
|
||||
handleSdmodelChange,
|
||||
handleSdt5xxlChange,
|
||||
handleSdcliplChange,
|
||||
handleSdclipgChange,
|
||||
handleSdphotomakerChange,
|
||||
handleSdvaeChange,
|
||||
handleSdloraChange,
|
||||
} = handlers;
|
||||
|
||||
return {
|
||||
handleModelPathChangeWithTracking: createChangeTracker(
|
||||
handleModelPathChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleGpuLayersChangeWithTracking: createChangeTracker(
|
||||
handleGpuLayersChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleAutoGpuLayersChangeWithTracking: createChangeTracker(
|
||||
handleAutoGpuLayersChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleContextSizeChangeWithTracking: createChangeTracker(
|
||||
handleContextSizeChangeWithStep,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleAdditionalArgumentsChangeWithTracking: createChangeTracker(
|
||||
handleAdditionalArgumentsChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handlePortChangeWithTracking: createChangeTracker(
|
||||
handlePortChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleHostChangeWithTracking: createChangeTracker(
|
||||
handleHostChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleNoshiftChangeWithTracking: createChangeTracker(
|
||||
handleNoshiftChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleFlashattentionChangeWithTracking: createChangeTracker(
|
||||
handleFlashattentionChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleNoavx2ChangeWithTracking: createChangeTracker(
|
||||
handleNoavx2Change,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleFailsafeChangeWithTracking: createChangeTracker(
|
||||
handleFailsafeChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleLowvramChangeWithTracking: createChangeTracker(
|
||||
handleLowvramChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleQuantmatmulChangeWithTracking: createChangeTracker(
|
||||
handleQuantmatmulChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleMultiuserChangeWithTracking: createChangeTracker(
|
||||
handleMultiuserChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleMultiplayerChangeWithTracking: createChangeTracker(
|
||||
handleMultiplayerChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleRemotetunnelChangeWithTracking: createChangeTracker(
|
||||
handleRemotetunnelChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleNocertifyChangeWithTracking: createChangeTracker(
|
||||
handleNocertifyChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleWebsearchChangeWithTracking: createChangeTracker(
|
||||
handleWebsearchChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleBackendChangeWithTracking: createChangeTracker(
|
||||
handleBackendChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdmodelChangeWithTracking: createChangeTracker(
|
||||
handleSdmodelChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdt5xxlChangeWithTracking: createChangeTracker(
|
||||
handleSdt5xxlChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdcliplChangeWithTracking: createChangeTracker(
|
||||
handleSdcliplChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdclipgChangeWithTracking: createChangeTracker(
|
||||
handleSdclipgChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdphotomakerChangeWithTracking: createChangeTracker(
|
||||
handleSdphotomakerChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdvaeChangeWithTracking: createChangeTracker(
|
||||
handleSdvaeChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
handleSdloraChangeWithTracking: createChangeTracker(
|
||||
handleSdloraChange,
|
||||
setHasUnsavedChanges
|
||||
),
|
||||
};
|
||||
};
|
||||
41
src/hooks/useWarnings.ts
Normal file
41
src/hooks/useWarnings.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
interface UseWarningsProps {
|
||||
modelPath: string;
|
||||
sdmodel: string;
|
||||
warnings: Array<{ type: 'warning' | 'info'; message: string }>;
|
||||
}
|
||||
|
||||
export const useWarnings = ({
|
||||
modelPath,
|
||||
sdmodel,
|
||||
warnings,
|
||||
}: UseWarningsProps) =>
|
||||
useMemo(() => {
|
||||
const hasTextModel = modelPath?.trim() !== '';
|
||||
const hasImageModel = sdmodel.trim() !== '';
|
||||
const showModelPriorityWarning = hasTextModel && hasImageModel;
|
||||
const showNoModelWarning = !hasTextModel && !hasImageModel;
|
||||
|
||||
return [
|
||||
...warnings,
|
||||
...(showModelPriorityWarning
|
||||
? [
|
||||
{
|
||||
type: 'warning' as const,
|
||||
message:
|
||||
'Both text and image generation models are selected. The image generation model will take priority and be used for launch.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showNoModelWarning
|
||||
? [
|
||||
{
|
||||
type: 'info' as const,
|
||||
message:
|
||||
'Select a model in the General or Image Generation tab to enable launch.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [modelPath, sdmodel, warnings]);
|
||||
|
|
@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client';
|
|||
import { MantineProvider } from '@mantine/core';
|
||||
import { App } from '@/App.tsx';
|
||||
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
||||
import './index.css';
|
||||
import '@/styles/index.css';
|
||||
|
||||
const AppWithTheme = () => {
|
||||
const { effectiveTheme } = useTheme();
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ export class KoboldCppManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
const configFileName = `${configName}.json`;
|
||||
const configFileName = `${configName}.kcpps`;
|
||||
const configPath = join(this.installDir, configFileName);
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
|
||||
|
|
@ -647,7 +647,7 @@ export class KoboldCppManager {
|
|||
return {
|
||||
name: ROCM.BINARY_NAME,
|
||||
url: ROCM.DOWNLOAD_URL,
|
||||
size: ROCM.SIZE_BYTES,
|
||||
size: ROCM.SIZE_BYTES_APPROX,
|
||||
version,
|
||||
type: 'rocm',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { Card, Container } from '@mantine/core';
|
||||
import { ServerTab } from '@/screens/Interface/ServerTab';
|
||||
import { TerminalTab } from '@/screens/Interface/TerminalTab';
|
||||
|
||||
interface InterfaceScreenProps {
|
||||
activeTab?: string | null;
|
||||
onTabChange?: (tab: string | null) => void;
|
||||
isImageGenerationMode?: boolean;
|
||||
}
|
||||
|
||||
export const InterfaceScreen = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
isImageGenerationMode = false,
|
||||
}: InterfaceScreenProps) => {
|
||||
const [serverUrl, setServerUrl] = useState<string>('');
|
||||
const [isServerReady, setIsServerReady] = useState<boolean>(false);
|
||||
|
||||
const handleServerReady = useCallback(
|
||||
(url: string) => {
|
||||
setServerUrl(url);
|
||||
setIsServerReady(true);
|
||||
if (onTabChange) {
|
||||
onTabChange(isImageGenerationMode ? 'image' : 'chat');
|
||||
}
|
||||
},
|
||||
[onTabChange, isImageGenerationMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container size="l" style={{ height: '85vh' }}>
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
p="0"
|
||||
style={{ height: 'calc(90vh - 32px)' }}
|
||||
>
|
||||
<div style={{ height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: activeTab === 'chat' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: activeTab === 'image' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ServerTab
|
||||
serverUrl={serverUrl}
|
||||
isServerReady={isServerReady}
|
||||
mode="image-generation"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: activeTab === 'terminal' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<TerminalTab onServerReady={handleServerReady} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
import { Text, Group, Select, Badge } from '@mantine/core';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
|
||||
interface BackendSelectorProps {
|
||||
backend: string;
|
||||
onBackendChange: (backend: string) => void;
|
||||
gpuDevice?: number;
|
||||
onGpuDeviceChange?: (device: number) => void;
|
||||
noavx2?: boolean;
|
||||
failsafe?: boolean;
|
||||
onWarningsChange?: (
|
||||
warnings: Array<{ type: 'warning' | 'info'; message: string }>
|
||||
) => void;
|
||||
onBackendsReady?: () => void;
|
||||
}
|
||||
|
||||
export const BackendSelector = ({
|
||||
backend,
|
||||
onBackendChange,
|
||||
gpuDevice = 0,
|
||||
onGpuDeviceChange,
|
||||
noavx2 = false,
|
||||
failsafe = false,
|
||||
onWarningsChange,
|
||||
onBackendsReady,
|
||||
}: BackendSelectorProps) => {
|
||||
const [availableBackends, setAvailableBackends] = useState<
|
||||
Array<{ value: string; label: string; devices?: string[] }>
|
||||
>([]);
|
||||
const [cpuCapabilities, setCpuCapabilities] = useState<{
|
||||
avx: boolean;
|
||||
avx2: boolean;
|
||||
} | null>(null);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) return;
|
||||
|
||||
let timeoutId: number;
|
||||
|
||||
const loadBackends = async () => {
|
||||
try {
|
||||
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentBinaryInfo(),
|
||||
window.electronAPI.kobold.detectCPU(),
|
||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||
]);
|
||||
|
||||
setCpuCapabilities({
|
||||
avx: cpuCapabilitiesResult.avx,
|
||||
avx2: cpuCapabilitiesResult.avx2,
|
||||
});
|
||||
|
||||
let backends: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
devices?: string[];
|
||||
}> = [];
|
||||
|
||||
if (currentBinaryInfo?.path) {
|
||||
backends = await window.electronAPI.kobold.getAvailableBackends(
|
||||
currentBinaryInfo.path,
|
||||
gpuCapabilities
|
||||
);
|
||||
|
||||
const cpuBackend = backends.find((b) => b.value === 'cpu');
|
||||
if (cpuBackend) {
|
||||
cpuBackend.devices = cpuCapabilitiesResult.devices;
|
||||
}
|
||||
}
|
||||
|
||||
setAvailableBackends(backends);
|
||||
hasInitialized.current = true;
|
||||
|
||||
if (backends.length > 0 && !backend) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
onBackendChange(backends[0].value);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
if (onBackendsReady) {
|
||||
window.setTimeout(() => {
|
||||
onBackendsReady();
|
||||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to detect available backends:',
|
||||
error as Error
|
||||
);
|
||||
setAvailableBackends([]);
|
||||
|
||||
if (onBackendsReady) {
|
||||
onBackendsReady();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadBackends();
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [backend, onBackendChange, onBackendsReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onWarningsChange) return;
|
||||
|
||||
if (backend !== 'cpu' || !cpuCapabilities) {
|
||||
onWarningsChange([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = [];
|
||||
|
||||
if (!cpuCapabilities.avx2 && !noavx2) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Your CPU does not support AVX2. Enable the "Disable AVX2" option to avoid crashes.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
|
||||
warnings.push({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option to avoid crashes.',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
availableBackends.length > 0 &&
|
||||
availableBackends.some((b) => b.value === 'cpu')
|
||||
) {
|
||||
warnings.push({
|
||||
type: 'info',
|
||||
message:
|
||||
"Performance Note: LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA or AMD's ROCm backends for optimal performance.",
|
||||
});
|
||||
}
|
||||
|
||||
onWarningsChange(warnings);
|
||||
}, [
|
||||
backend,
|
||||
cpuCapabilities,
|
||||
noavx2,
|
||||
failsafe,
|
||||
availableBackends,
|
||||
onWarningsChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Backend
|
||||
</Text>
|
||||
<InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
|
||||
</Group>
|
||||
<Select
|
||||
placeholder="Select backend"
|
||||
value={backend}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
onBackendChange(value);
|
||||
}
|
||||
}}
|
||||
data={availableBackends.map((b) => ({
|
||||
value: b.value,
|
||||
label: b.label,
|
||||
}))}
|
||||
disabled={availableBackends.length === 0}
|
||||
comboboxProps={{
|
||||
middlewares: {
|
||||
flip: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{(backend === 'cuda' || backend === 'rocm') &&
|
||||
onGpuDeviceChange &&
|
||||
availableBackends.find((b) => b.value === backend)?.devices &&
|
||||
availableBackends.find((b) => b.value === backend)!.devices!.length >
|
||||
1 && (
|
||||
<Select
|
||||
label="GPU Device"
|
||||
placeholder="Select GPU device"
|
||||
value={gpuDevice.toString()}
|
||||
onChange={(value) =>
|
||||
value && onGpuDeviceChange(parseInt(value, 10))
|
||||
}
|
||||
data={availableBackends
|
||||
.find((b) => b.value === backend)!
|
||||
.devices!.map((device, index) => ({
|
||||
value: index.toString(),
|
||||
label: `GPU ${index}: ${device}`,
|
||||
}))}
|
||||
mt="xs"
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableBackends.find((b) => b.value === backend)?.devices && (
|
||||
<Group gap="xs" mt="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
{availableBackends.find((b) => b.value === backend)?.devices
|
||||
?.length === 1
|
||||
? 'Device:'
|
||||
: 'Devices:'}
|
||||
</Text>
|
||||
{availableBackends
|
||||
.find((b) => b.value === backend)
|
||||
?.devices?.map((device, index) => (
|
||||
<Badge key={index} variant="light" size="sm">
|
||||
{device}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import { Stack, Text, Group, Button, Menu, Select, Badge } from '@mantine/core';
|
||||
import { Save, Settings2, File } from 'lucide-react';
|
||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface ConfigurationManagerProps {
|
||||
configFiles: ConfigFile[];
|
||||
selectedFile: string | null;
|
||||
onFileSelection: (fileName: string) => void;
|
||||
onSaveAsNew: () => void;
|
||||
onUpdateCurrent: () => void;
|
||||
}
|
||||
|
||||
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
||||
label: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
const getBadgeColor = (extension: string) => {
|
||||
switch (extension.toLowerCase()) {
|
||||
case '.kcpps':
|
||||
return 'blue';
|
||||
case '.kcppt':
|
||||
return 'green';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
({ label, extension, ...others }, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" truncate>
|
||||
{label}
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
|
||||
{extension}
|
||||
</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
SelectItem.displayName = 'SelectItem';
|
||||
|
||||
export const ConfigurationManager = ({
|
||||
configFiles,
|
||||
selectedFile,
|
||||
onFileSelection,
|
||||
onSaveAsNew,
|
||||
onUpdateCurrent,
|
||||
}: ConfigurationManagerProps) => (
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={500}>Configuration File</Text>
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Button variant="light" leftSection={<Save size={16} />}>
|
||||
Save
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
|
||||
Save as new configuration
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<Settings2 size={16} />}
|
||||
disabled={!selectedFile}
|
||||
onClick={onUpdateCurrent}
|
||||
>
|
||||
Update current configuration
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
|
||||
{configFiles.length === 0 ? (
|
||||
<Text c="dimmed" ta="center">
|
||||
No configuration files found in the installation directory.
|
||||
<br />
|
||||
Please ensure your .kcpps or .kcppt files are in the correct location.
|
||||
</Text>
|
||||
) : (
|
||||
(() => {
|
||||
const selectData = configFiles.map((file) => {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
const nameWithoutExtension = file.name.replace(`.${extension}`, '');
|
||||
|
||||
return {
|
||||
value: file.name,
|
||||
label: nameWithoutExtension,
|
||||
extension: `.${extension}`,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder="Select a configuration file"
|
||||
value={selectedFile}
|
||||
onChange={(value) => value && onFileSelection(value)}
|
||||
data={selectData}
|
||||
leftSection={<File size={16} />}
|
||||
searchable
|
||||
clearable={false}
|
||||
w="100%"
|
||||
filter={({ options, search }) =>
|
||||
options.filter((option) => {
|
||||
if ('label' in option) {
|
||||
return option.label
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase().trim());
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}
|
||||
renderOption={({ option }) => {
|
||||
const dataItem = selectData.find(
|
||||
(item) => item.value === option.value
|
||||
);
|
||||
const extension = dataItem?.extension || '';
|
||||
return <SelectItem label={option.label} extension={extension} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
|
@ -1,787 +0,0 @@
|
|||
import {
|
||||
Card,
|
||||
Container,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
Select,
|
||||
Modal,
|
||||
TextInput,
|
||||
Badge,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
} from 'react';
|
||||
import { Save, File, Plus } from 'lucide-react';
|
||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||
import { useChangeTracker } from '@/hooks/useChangeTracker';
|
||||
import { GeneralTab } from '@/screens/Launch/GeneralTab';
|
||||
import { AdvancedTab } from '@/screens/Launch/AdvancedTab';
|
||||
import { NetworkTab } from '@/screens/Launch/NetworkTab';
|
||||
import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab';
|
||||
import { WarningDisplay } from '@/components/WarningDisplay';
|
||||
import type { ConfigFile } from '@/types';
|
||||
|
||||
interface LaunchScreenProps {
|
||||
onLaunch: () => void;
|
||||
onLaunchModeChange?: (isImageMode: boolean) => void;
|
||||
}
|
||||
|
||||
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
|
||||
label: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
const getBadgeColor = (extension: string) => {
|
||||
switch (extension.toLowerCase()) {
|
||||
case '.kcpps':
|
||||
return 'blue';
|
||||
case '.kcppt':
|
||||
return 'green';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
({ label, extension, ...others }, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm" truncate>
|
||||
{label}
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
|
||||
{extension}
|
||||
</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
SelectItem.displayName = 'SelectItem';
|
||||
|
||||
export const LaunchScreen = ({
|
||||
onLaunch,
|
||||
onLaunchModeChange,
|
||||
}: LaunchScreenProps) => {
|
||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [, setInstallDir] = useState<string>('');
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<string | null>('general');
|
||||
const [saveAsModalOpened, setSaveAsModalOpened] = useState(false);
|
||||
const [newConfigName, setNewConfigName] = useState('');
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const [warnings, setWarnings] = useState<
|
||||
Array<{ type: 'warning' | 'info'; message: string }>
|
||||
>([]);
|
||||
|
||||
const {
|
||||
gpuLayers,
|
||||
autoGpuLayers,
|
||||
contextSize,
|
||||
modelPath,
|
||||
additionalArguments,
|
||||
port,
|
||||
host,
|
||||
multiuser,
|
||||
multiplayer,
|
||||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
noshift,
|
||||
flashattention,
|
||||
noavx2,
|
||||
failsafe,
|
||||
lowvram,
|
||||
quantmatmul,
|
||||
backend,
|
||||
gpuDevice,
|
||||
sdmodel,
|
||||
sdt5xxl,
|
||||
sdclipl,
|
||||
sdclipg,
|
||||
sdphotomaker,
|
||||
sdvae,
|
||||
sdlora,
|
||||
parseAndApplyConfigFile,
|
||||
loadSavedSettings,
|
||||
loadConfigFromFile,
|
||||
handleGpuLayersChange,
|
||||
handleAutoGpuLayersChange,
|
||||
handleContextSizeChangeWithStep,
|
||||
handleModelPathChange,
|
||||
handleSelectModelFile,
|
||||
handleAdditionalArgumentsChange,
|
||||
handlePortChange,
|
||||
handleHostChange,
|
||||
handleMultiuserChange,
|
||||
handleMultiplayerChange,
|
||||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleNoavx2Change,
|
||||
handleFailsafeChange,
|
||||
handleLowvramChange,
|
||||
handleQuantmatmulChange,
|
||||
handleBackendChange,
|
||||
handleGpuDeviceChange,
|
||||
handleSdmodelChange,
|
||||
handleSelectSdmodelFile,
|
||||
handleSdt5xxlChange,
|
||||
handleSelectSdt5xxlFile,
|
||||
handleSdcliplChange,
|
||||
handleSelectSdcliplFile,
|
||||
handleSdclipgChange,
|
||||
handleSelectSdclipgFile,
|
||||
handleSdphotomakerChange,
|
||||
handleSelectSdphotomakerFile,
|
||||
handleSdvaeChange,
|
||||
handleSelectSdvaeFile,
|
||||
handleSdloraChange,
|
||||
handleSelectSdloraFile,
|
||||
handleApplyPreset,
|
||||
} = useLaunchConfig();
|
||||
|
||||
const createChangeTracker = useChangeTracker;
|
||||
|
||||
const handleModelPathChangeWithTracking = createChangeTracker(
|
||||
handleModelPathChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleGpuLayersChangeWithTracking = createChangeTracker(
|
||||
handleGpuLayersChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleAutoGpuLayersChangeWithTracking = createChangeTracker(
|
||||
handleAutoGpuLayersChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleContextSizeChangeWithTracking = createChangeTracker(
|
||||
handleContextSizeChangeWithStep,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleAdditionalArgumentsChangeWithTracking = createChangeTracker(
|
||||
handleAdditionalArgumentsChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handlePortChangeWithTracking = createChangeTracker(
|
||||
handlePortChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleHostChangeWithTracking = createChangeTracker(
|
||||
handleHostChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleNoshiftChangeWithTracking = createChangeTracker(
|
||||
handleNoshiftChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleFlashattentionChangeWithTracking = createChangeTracker(
|
||||
handleFlashattentionChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleNoavx2ChangeWithTracking = createChangeTracker(
|
||||
handleNoavx2Change,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleFailsafeChangeWithTracking = createChangeTracker(
|
||||
handleFailsafeChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleLowvramChangeWithTracking = createChangeTracker(
|
||||
handleLowvramChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleQuantmatmulChangeWithTracking = createChangeTracker(
|
||||
handleQuantmatmulChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleMultiuserChangeWithTracking = createChangeTracker(
|
||||
handleMultiuserChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleMultiplayerChangeWithTracking = createChangeTracker(
|
||||
handleMultiplayerChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleRemotetunnelChangeWithTracking = createChangeTracker(
|
||||
handleRemotetunnelChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleNocertifyChangeWithTracking = createChangeTracker(
|
||||
handleNocertifyChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleWebsearchChangeWithTracking = createChangeTracker(
|
||||
handleWebsearchChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleBackendChangeWithTracking = createChangeTracker(
|
||||
handleBackendChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdmodelChangeWithTracking = createChangeTracker(
|
||||
handleSdmodelChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdt5xxlChangeWithTracking = createChangeTracker(
|
||||
handleSdt5xxlChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdcliplChangeWithTracking = createChangeTracker(
|
||||
handleSdcliplChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdclipgChangeWithTracking = createChangeTracker(
|
||||
handleSdclipgChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdphotomakerChangeWithTracking = createChangeTracker(
|
||||
handleSdphotomakerChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdvaeChangeWithTracking = createChangeTracker(
|
||||
handleSdvaeChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
const handleSdloraChangeWithTracking = createChangeTracker(
|
||||
handleSdloraChange,
|
||||
setHasUnsavedChanges
|
||||
);
|
||||
|
||||
const loadConfigFiles = useCallback(async () => {
|
||||
const [files, currentDir, savedConfig] = await Promise.all([
|
||||
window.electronAPI.kobold.getConfigFiles(),
|
||||
window.electronAPI.kobold.getCurrentInstallDir(),
|
||||
window.electronAPI.kobold.getSelectedConfig(),
|
||||
]);
|
||||
|
||||
setConfigFiles(files);
|
||||
setInstallDir(currentDir);
|
||||
|
||||
if (savedConfig && files.some((f) => f.name === savedConfig)) {
|
||||
setSelectedFile(savedConfig);
|
||||
} else if (files.length > 0 && !selectedFile) {
|
||||
setSelectedFile(files[0].name);
|
||||
}
|
||||
|
||||
await loadSavedSettings();
|
||||
|
||||
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
|
||||
if (currentSelectedFile && !selectedFile) {
|
||||
setSelectedFile(currentSelectedFile);
|
||||
}
|
||||
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
|
||||
const handleFileSelection = async (fileName: string) => {
|
||||
setSelectedFile(fileName);
|
||||
await window.electronAPI.kobold.setSelectedConfig(fileName);
|
||||
|
||||
const selectedConfig = configFiles.find((f) => f.name === fileName);
|
||||
if (selectedConfig) {
|
||||
await parseAndApplyConfigFile(selectedConfig.path);
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfigFiles();
|
||||
|
||||
const handleInstallDirChange = () => {
|
||||
void loadConfigFiles();
|
||||
};
|
||||
|
||||
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
|
||||
handleInstallDirChange
|
||||
);
|
||||
|
||||
return cleanup;
|
||||
}, [loadConfigFiles]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleLaunch = async () => {
|
||||
const isImageMode = sdmodel.trim() !== '';
|
||||
const isTextMode = modelPath.trim() !== '';
|
||||
|
||||
if (isLaunching || (!isImageMode && !isTextMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLaunching(true);
|
||||
|
||||
try {
|
||||
const selectedConfig = selectedFile
|
||||
? configFiles.find((f) => f.name === selectedFile)
|
||||
: null;
|
||||
|
||||
const args: string[] = [];
|
||||
|
||||
if (isImageMode && isTextMode) {
|
||||
args.push('--sdmodel', sdmodel);
|
||||
} else if (isImageMode) {
|
||||
args.push('--sdmodel', sdmodel);
|
||||
|
||||
if (sdt5xxl.trim()) {
|
||||
args.push('--sdt5xxl', sdt5xxl);
|
||||
}
|
||||
|
||||
if (sdclipl.trim()) {
|
||||
args.push('--sdclipl', sdclipl);
|
||||
}
|
||||
|
||||
if (sdclipg.trim()) {
|
||||
args.push('--sdclipg', sdclipg);
|
||||
}
|
||||
|
||||
if (sdphotomaker.trim()) {
|
||||
args.push('--sdphotomaker', sdphotomaker);
|
||||
}
|
||||
|
||||
if (sdvae.trim()) {
|
||||
args.push('--sdvae', sdvae);
|
||||
}
|
||||
|
||||
if (sdlora.trim()) {
|
||||
args.push('--sdlora', sdlora);
|
||||
}
|
||||
} else {
|
||||
args.push('--model', modelPath);
|
||||
}
|
||||
|
||||
if (autoGpuLayers) {
|
||||
args.push('--gpulayers', '-1');
|
||||
} else if (gpuLayers > 0) {
|
||||
args.push('--gpulayers', gpuLayers.toString());
|
||||
}
|
||||
|
||||
if (contextSize) {
|
||||
args.push('--contextsize', contextSize.toString());
|
||||
}
|
||||
|
||||
if (port) {
|
||||
args.push('--port', port.toString());
|
||||
}
|
||||
|
||||
if (host !== 'localhost' && host) {
|
||||
args.push('--host', host);
|
||||
}
|
||||
|
||||
if (multiuser) {
|
||||
args.push('--multiuser', '1');
|
||||
}
|
||||
|
||||
if (multiplayer) {
|
||||
args.push('--multiplayer');
|
||||
}
|
||||
|
||||
if (remotetunnel) {
|
||||
args.push('--remotetunnel');
|
||||
}
|
||||
|
||||
if (nocertify) {
|
||||
args.push('--nocertify');
|
||||
}
|
||||
|
||||
if (websearch) {
|
||||
args.push('--websearch');
|
||||
}
|
||||
|
||||
if (noshift) {
|
||||
args.push('--noshift');
|
||||
}
|
||||
|
||||
if (flashattention) {
|
||||
args.push('--flashattention');
|
||||
}
|
||||
|
||||
if (backend && backend !== 'cpu') {
|
||||
if (backend === 'cuda' || backend === 'rocm') {
|
||||
const cudaArgs = ['--usecuda'];
|
||||
|
||||
cudaArgs.push(lowvram ? 'lowvram' : 'normal');
|
||||
cudaArgs.push(gpuDevice.toString());
|
||||
cudaArgs.push(quantmatmul ? 'mmq' : 'nommq');
|
||||
|
||||
args.push(...cudaArgs);
|
||||
} else if (backend === 'vulkan') {
|
||||
args.push('--usevulkan');
|
||||
} else if (backend === 'clblast') {
|
||||
args.push('--useclblast');
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalArguments.trim()) {
|
||||
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
||||
args.push(...additionalArgs);
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.kobold.launchKoboldCpp(
|
||||
args,
|
||||
selectedConfig?.path
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
if (onLaunchModeChange) {
|
||||
onLaunchModeChange(isImageMode);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
onLaunch();
|
||||
}, 100);
|
||||
} else {
|
||||
window.electronAPI.logs.logError(
|
||||
'Launch failed:',
|
||||
new Error(result.error)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Error launching KoboldCpp:',
|
||||
error as Error
|
||||
);
|
||||
} finally {
|
||||
setIsLaunching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasTextModel = modelPath?.trim() !== '';
|
||||
const hasImageModel = sdmodel.trim() !== '';
|
||||
const showModelPriorityWarning = hasTextModel && hasImageModel;
|
||||
const showNoModelWarning = !hasTextModel && !hasImageModel;
|
||||
|
||||
const combinedWarnings = [
|
||||
...warnings,
|
||||
...(showModelPriorityWarning
|
||||
? [
|
||||
{
|
||||
type: 'warning' as const,
|
||||
message:
|
||||
'Both text and image generation models are selected. The image generation model will take priority and be used for launch.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showNoModelWarning
|
||||
? [
|
||||
{
|
||||
type: 'info' as const,
|
||||
message:
|
||||
'Select a model in the General or Image Generation tab to enable launch.',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="sm">
|
||||
<Stack gap="md">
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Stack gap="xs">
|
||||
<Text fw={500} size="sm">
|
||||
Configuration File
|
||||
</Text>
|
||||
{configFiles.length === 0 ? (
|
||||
<Group gap="xs" align="flex-end">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text c="dimmed" size="sm">
|
||||
No saved configurations. Create your first configuration
|
||||
by clicking “New” then “Save”.
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<Plus size={14} />}
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
setSelectedFile(null);
|
||||
await loadSavedSettings();
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
(() => {
|
||||
const selectData = configFiles.map((file) => {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
const nameWithoutExtension = file.name.replace(
|
||||
`.${extension}`,
|
||||
''
|
||||
);
|
||||
|
||||
return {
|
||||
value: file.name,
|
||||
label: nameWithoutExtension,
|
||||
extension: `.${extension}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (selectedFile === null && hasUnsavedChanges) {
|
||||
selectData.unshift({
|
||||
value: '__new__',
|
||||
label: 'New Configuration (unsaved)',
|
||||
extension: '.kcpps',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Group gap="xs" align="flex-end">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Select
|
||||
placeholder="Select a configuration file"
|
||||
value={
|
||||
selectedFile === null && hasUnsavedChanges
|
||||
? '__new__'
|
||||
: selectedFile
|
||||
}
|
||||
onChange={(value: string | null) => {
|
||||
if (value === '__new__') {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
handleFileSelection(value);
|
||||
}
|
||||
}}
|
||||
data={selectData}
|
||||
leftSection={<File size={16} />}
|
||||
searchable
|
||||
clearable={false}
|
||||
renderOption={({ option }) => {
|
||||
const dataItem = selectData.find(
|
||||
(item) => item.value === option.value
|
||||
);
|
||||
const extension = dataItem?.extension || '';
|
||||
return (
|
||||
<SelectItem
|
||||
label={option.label}
|
||||
extension={extension}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant={
|
||||
selectedFile === null && hasUnsavedChanges
|
||||
? 'filled'
|
||||
: 'light'
|
||||
}
|
||||
leftSection={<Plus size={14} />}
|
||||
size="sm"
|
||||
disabled={selectedFile === null && hasUnsavedChanges}
|
||||
onClick={async () => {
|
||||
setSelectedFile(null);
|
||||
await loadSavedSettings();
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
{selectedFile === null && hasUnsavedChanges
|
||||
? 'Creating New...'
|
||||
: 'New'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
leftSection={<Save size={14} />}
|
||||
size="sm"
|
||||
disabled={!hasUnsavedChanges}
|
||||
onClick={() => {
|
||||
if (selectedFile) {
|
||||
setHasUnsavedChanges(false);
|
||||
} else {
|
||||
setSaveAsModalOpened(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||
<Tabs.Tab value="image">Image Generation</Tabs.Tab>
|
||||
<Tabs.Tab value="network">Network</Tabs.Tab>
|
||||
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<div style={{ minHeight: '300px' }}>
|
||||
<Tabs.Panel value="general" pt="md">
|
||||
<GeneralTab
|
||||
modelPath={modelPath}
|
||||
gpuLayers={gpuLayers}
|
||||
autoGpuLayers={autoGpuLayers}
|
||||
contextSize={contextSize}
|
||||
backend={backend}
|
||||
gpuDevice={gpuDevice}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
onModelPathChange={handleModelPathChangeWithTracking}
|
||||
onSelectModelFile={handleSelectModelFile}
|
||||
onGpuLayersChange={handleGpuLayersChangeWithTracking}
|
||||
onAutoGpuLayersChange={
|
||||
handleAutoGpuLayersChangeWithTracking
|
||||
}
|
||||
onContextSizeChange={handleContextSizeChangeWithTracking}
|
||||
onBackendChange={handleBackendChangeWithTracking}
|
||||
onGpuDeviceChange={handleGpuDeviceChange}
|
||||
onWarningsChange={setWarnings}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="advanced" pt="md">
|
||||
<AdvancedTab
|
||||
additionalArguments={additionalArguments}
|
||||
noshift={noshift}
|
||||
flashattention={flashattention}
|
||||
noavx2={noavx2}
|
||||
failsafe={failsafe}
|
||||
lowvram={lowvram}
|
||||
quantmatmul={quantmatmul}
|
||||
backend={backend}
|
||||
onAdditionalArgumentsChange={
|
||||
handleAdditionalArgumentsChangeWithTracking
|
||||
}
|
||||
onNoshiftChange={handleNoshiftChangeWithTracking}
|
||||
onFlashattentionChange={
|
||||
handleFlashattentionChangeWithTracking
|
||||
}
|
||||
onNoavx2Change={handleNoavx2ChangeWithTracking}
|
||||
onFailsafeChange={handleFailsafeChangeWithTracking}
|
||||
onLowvramChange={handleLowvramChangeWithTracking}
|
||||
onQuantmatmulChange={handleQuantmatmulChangeWithTracking}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="network" pt="md">
|
||||
<NetworkTab
|
||||
port={port}
|
||||
host={host}
|
||||
multiuser={multiuser}
|
||||
multiplayer={multiplayer}
|
||||
remotetunnel={remotetunnel}
|
||||
nocertify={nocertify}
|
||||
websearch={websearch}
|
||||
onPortChange={handlePortChangeWithTracking}
|
||||
onHostChange={handleHostChangeWithTracking}
|
||||
onMultiuserChange={handleMultiuserChangeWithTracking}
|
||||
onMultiplayerChange={handleMultiplayerChangeWithTracking}
|
||||
onRemotetunnelChange={handleRemotetunnelChangeWithTracking}
|
||||
onNocertifyChange={handleNocertifyChangeWithTracking}
|
||||
onWebsearchChange={handleWebsearchChangeWithTracking}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="image" pt="md">
|
||||
<ImageGenerationTab
|
||||
sdmodel={sdmodel}
|
||||
sdt5xxl={sdt5xxl}
|
||||
sdclipl={sdclipl}
|
||||
sdclipg={sdclipg}
|
||||
sdphotomaker={sdphotomaker}
|
||||
sdvae={sdvae}
|
||||
sdlora={sdlora}
|
||||
onSdmodelChange={handleSdmodelChangeWithTracking}
|
||||
onSelectSdmodelFile={handleSelectSdmodelFile}
|
||||
onSdt5xxlChange={handleSdt5xxlChangeWithTracking}
|
||||
onSelectSdt5xxlFile={handleSelectSdt5xxlFile}
|
||||
onSdcliplChange={handleSdcliplChangeWithTracking}
|
||||
onSelectSdcliplFile={handleSelectSdcliplFile}
|
||||
onSdclipgChange={handleSdclipgChangeWithTracking}
|
||||
onSelectSdclipgFile={handleSelectSdclipgFile}
|
||||
onSdphotomakerChange={handleSdphotomakerChangeWithTracking}
|
||||
onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
|
||||
onSdvaeChange={handleSdvaeChangeWithTracking}
|
||||
onSelectSdvaeFile={handleSelectSdvaeFile}
|
||||
onSdloraChange={handleSdloraChangeWithTracking}
|
||||
onSelectSdloraFile={handleSelectSdloraFile}
|
||||
onApplyPreset={handleApplyPreset}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
<Group justify="flex-end" pt="md">
|
||||
<WarningDisplay warnings={combinedWarnings}>
|
||||
<Button
|
||||
radius="md"
|
||||
disabled={(!modelPath && !sdmodel) || isLaunching}
|
||||
onClick={handleLaunch}
|
||||
size="lg"
|
||||
variant="filled"
|
||||
color="blue"
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
padding: '12px 28px',
|
||||
minWidth: '120px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
</WarningDisplay>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
opened={saveAsModalOpened}
|
||||
onClose={() => setSaveAsModalOpened(false)}
|
||||
title="Save Configuration As..."
|
||||
size="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Configuration Name"
|
||||
placeholder="Enter a name for this configuration"
|
||||
value={newConfigName}
|
||||
onChange={(event) => setNewConfigName(event.currentTarget.value)}
|
||||
data-autofocus
|
||||
/>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSaveAsModalOpened(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!newConfigName.trim()}
|
||||
leftSection={<Save size={16} />}
|
||||
onClick={() => {
|
||||
setSaveAsModalOpened(false);
|
||||
setNewConfigName('');
|
||||
setHasUnsavedChanges(false);
|
||||
}}
|
||||
>
|
||||
Save As New
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
14
src/styles/layout.module.css
Normal file
14
src/styles/layout.module.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.minWidth200 {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.terminalScrollArea {
|
||||
flex: 1;
|
||||
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
|
|
@ -18,6 +18,7 @@ export interface GitHubRelease {
|
|||
name: string;
|
||||
published_at: string;
|
||||
body: string;
|
||||
html_url: string;
|
||||
assets: GitHubAsset[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,3 +7,9 @@ export const formatFileSize = (bytes: number) => {
|
|||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizeUnit;
|
||||
};
|
||||
|
||||
export const formatFileSizeInMB = (bytes: number) => {
|
||||
if (bytes === 0) return '0 MB';
|
||||
const mb = bytes / (1024 * 1024);
|
||||
return parseFloat(mb.toFixed(1)) + ' MB';
|
||||
};
|
||||
|
|
|
|||
142
yarn.lock
142
yarn.lock
|
|
@ -2054,106 +2054,106 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:^8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.39.1"
|
||||
"@typescript-eslint/eslint-plugin@npm:^8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.40.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.39.1"
|
||||
"@typescript-eslint/type-utils": "npm:8.39.1"
|
||||
"@typescript-eslint/utils": "npm:8.39.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.40.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.40.0"
|
||||
"@typescript-eslint/utils": "npm:8.40.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||
graphemer: "npm:^1.4.0"
|
||||
ignore: "npm:^7.0.0"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.39.1
|
||||
"@typescript-eslint/parser": ^8.40.0
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/7a55de558ed6ea6f09ee0b0d994b4a70e1df9f72e4afc7b3073de1b41504a36d905779304d59c34db700af60da3bb438c62480d30462a13b8b72d0b50318aeee
|
||||
checksum: 10c0/dc8889c3255bce6956432f099059179dd13826ba29670f81ba9238ecde46764ee63459eb73a7d88f4f30e1144a2f000d79c9e3f256fa759689d9b3b74d423bda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:^8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/parser@npm:8.39.1"
|
||||
"@typescript-eslint/parser@npm:^8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.40.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.39.1"
|
||||
"@typescript-eslint/types": "npm:8.39.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.39.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.40.0"
|
||||
"@typescript-eslint/types": "npm:8.40.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.40.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/da30372c4e8dee48a0c421996bf0bf73a62a57039ee6b817eda64de2d70fdb88dd20b50615c81be7e68fd29cdd7852829b859bb8539b4a4c78030f93acaf5664
|
||||
checksum: 10c0/43ca9589b8a1f3f4b30a214c0e2254fa0ad43458ef1258b1d62c5aad52710ad11b9315b124cda79163274147b82201a5d76fab7de413e34bfe8e377142b71e98
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/project-service@npm:8.39.1"
|
||||
"@typescript-eslint/project-service@npm:8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.40.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.39.1"
|
||||
"@typescript-eslint/types": "npm:^8.39.1"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.40.0"
|
||||
"@typescript-eslint/types": "npm:^8.40.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/40207af4f4e2a260ea276766d502c4736f6dc5488e84bbab6444e2786289ece2dbca2686323c48d4e9c265e409a309bf3d97d4aa03767dff8cc7642b436bda35
|
||||
checksum: 10c0/23d62e9ada9750136d0251f268bbe1f9784442ef258bb340a2e1e866749d8076730a14749d9a320d94d7c76df2d108caf21fe35e5dc100385f04be846dc979cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.39.1"
|
||||
"@typescript-eslint/scope-manager@npm:8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.40.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.39.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
||||
checksum: 10c0/9466db557c1a0eaaf24b0ece5810413d11390d046bf6e47c4074879e8dba0348b835a21106c842ab20ff85f2384312cf9e20bfe7684e31640696e29957003511
|
||||
"@typescript-eslint/types": "npm:8.40.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||
checksum: 10c0/48af81f9cdcec466994d290561e8d2fa3f6b156a898b71dd0e65633c896543b44729c5353596e84de2ae61bfd20e1398c3309cdfe86714a9663fd5aded4c9cd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.39.1, @typescript-eslint/tsconfig-utils@npm:^8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.39.1"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.40.0, @typescript-eslint/tsconfig-utils@npm:^8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.40.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/664dff0b4ae908cb98c78f9ca73c36cf57c3a2206965d9d0659649ffc02347eb30e1452499671a425592f14a2a5c5eb82ae389b34f3c415a12119506b4ebb61c
|
||||
checksum: 10c0/c2366dcd802901d5cd4f59fc4eab7a00ed119aa4591ba59c507fe495d9af4cfca19431a603602ea675e4c861962230d1c2f100896903750cd1fcfc134702a7d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.39.1"
|
||||
"@typescript-eslint/type-utils@npm:8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.40.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.39.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.39.1"
|
||||
"@typescript-eslint/utils": "npm:8.39.1"
|
||||
"@typescript-eslint/types": "npm:8.40.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.40.0"
|
||||
"@typescript-eslint/utils": "npm:8.40.0"
|
||||
debug: "npm:^4.3.4"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/430dfefe040eae5f0c8dfbce37b5ce071095a28f335e74793923d113682e26313586e90f7bbe2c2f9bffb0da52ffdf5055ea36b96d9f218cef35aa14853122d5
|
||||
checksum: 10c0/660b77d801b2538a4ccb65065269ad0e8370d0be985172b5ecb067f3eea22e64aa8af9e981b31bf2a34002339fe3253b09b55d181ce6d8242fc7daa80ac4aaca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.39.1, @typescript-eslint/types@npm:^8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/types@npm:8.39.1"
|
||||
checksum: 10c0/0e188d2d52509a24c500a87adf561387ffcac56b62cb9fd0ca1f929bb3d4eedb6b8f9d516c1890855d39930c9dd8d502d5b4600b8c9cc832d3ebb595d81c7533
|
||||
"@typescript-eslint/types@npm:8.40.0, @typescript-eslint/types@npm:^8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/types@npm:8.40.0"
|
||||
checksum: 10c0/225374fff36d59288a5780667a7a1316c75090d5d60b70a8035ac18786120333ccd08dfdf0e05e30d5a82217e44c57b8708b769dd1eed89f12f2ac4d3a769f76
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.39.1"
|
||||
"@typescript-eslint/typescript-estree@npm:8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.40.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.39.1"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.39.1"
|
||||
"@typescript-eslint/types": "npm:8.39.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
||||
"@typescript-eslint/project-service": "npm:8.40.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.40.0"
|
||||
"@typescript-eslint/types": "npm:8.40.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
|
|
@ -2162,32 +2162,32 @@ __metadata:
|
|||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/1de1a37fed354600a08bc971492c2f14238f0a4bf07a43bedb416c17b7312d18bec92c68c8f2790bb0a1bffcd757f7962914be9f6213068f18f6c4fdde259af4
|
||||
checksum: 10c0/6c1ffc17947cb36cbd987cf9705f85223ed1cce584b5244840e36a2b8480861f4dfdb0312f96afbc12e7d1ba586005f0d959042baa0a96a1913ac7ace8e8f6d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/utils@npm:8.39.1"
|
||||
"@typescript-eslint/utils@npm:8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.40.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.39.1"
|
||||
"@typescript-eslint/types": "npm:8.39.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.39.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.40.0"
|
||||
"@typescript-eslint/types": "npm:8.40.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.40.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/ebc01d736af43728df9a0915058d0c771dec9cc58846ffdcbb986c78e7dabf547ea7daecd75db58b2af88a3c2a43de8a7e5f81feefacfa31be173fc384d25d77
|
||||
checksum: 10c0/6b3858b8725083fe7db7fb9bcbde930e758a6ba8ddedd1ed27d828fc1cbe04f54b774ef9144602f8eeaafeea9b19b4fd4c46fdad52a10ade99e6b282c7d0df92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.39.1":
|
||||
version: 8.39.1
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.39.1"
|
||||
"@typescript-eslint/visitor-keys@npm:8.40.0":
|
||||
version: 8.40.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.40.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.39.1"
|
||||
"@typescript-eslint/types": "npm:8.40.0"
|
||||
eslint-visitor-keys: "npm:^4.2.1"
|
||||
checksum: 10c0/4d81f6826a211bc2752e25cd16d1f415f28ebc92b35142402ec23f3765f2d00963b75ac06266ad9c674ca5b057d07d8c114116e5bf14f5465dde1d1aa60bc72f
|
||||
checksum: 10c0/592f1c8c2d3da43a7f74f8ead14f05fafc2e4609d5df36811cf92ead5dc94f6f669556a494048e4746cb3774c60bc52a8c83d75369d5e196778d935c70e7d3a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -4431,8 +4431,8 @@ __metadata:
|
|||
"@types/node": "npm:^24.3.0"
|
||||
"@types/react": "npm:^19.1.10"
|
||||
"@types/react-dom": "npm:^19.1.7"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.39.1"
|
||||
"@typescript-eslint/parser": "npm:^8.39.1"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.40.0"
|
||||
"@typescript-eslint/parser": "npm:^8.40.0"
|
||||
"@vitejs/plugin-react": "npm:^5.0.0"
|
||||
cross-env: "npm:^10.0.0"
|
||||
cspell: "npm:^9.2.0"
|
||||
|
|
@ -4450,7 +4450,7 @@ __metadata:
|
|||
husky: "npm:^9.1.7"
|
||||
jiti: "npm:^2.5.1"
|
||||
lint-staged: "npm:^16.1.5"
|
||||
lucide-react: "npm:^0.539.0"
|
||||
lucide-react: "npm:^0.540.0"
|
||||
prettier: "npm:^3.6.2"
|
||||
react: "npm:^19.1.1"
|
||||
react-dom: "npm:^19.1.1"
|
||||
|
|
@ -5782,12 +5782,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lucide-react@npm:^0.539.0":
|
||||
version: 0.539.0
|
||||
resolution: "lucide-react@npm:0.539.0"
|
||||
"lucide-react@npm:^0.540.0":
|
||||
version: 0.540.0
|
||||
resolution: "lucide-react@npm:0.540.0"
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/ae85244dc23a78c51cc5f829ee09321cf68201333ab3d5614ed73cda77a4cd12284d3630a5a2167a39e31e02811056336de76a597c6874b28bb9984baf876494
|
||||
checksum: 10c0/f4dc8a540b1b079958fdf7a1804dfb3f9093a9393f0c0a4b32a7e839869df5e29946c4fe226f06edd3056f6eccfa77b53e44be89f42379e7fe31c4e59caee3fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue