mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-04 04:04: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
7d833b96e6
commit
fc2ca9d665
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
|
### 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.
|
- 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/node": "^24.3.0",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
||||||
"@typescript-eslint/parser": "^8.39.1",
|
"@typescript-eslint/parser": "^8.40.0",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"cspell": "^9.2.0",
|
"cspell": "^9.2.0",
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/core": "^8.2.5",
|
"@mantine/core": "^8.2.5",
|
||||||
"@mantine/hooks": "^8.2.5",
|
"@mantine/hooks": "^8.2.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.540.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"systeminformation": "^5.27.7"
|
"systeminformation": "^5.27.7"
|
||||||
|
|
|
||||||
14
src/App.tsx
14
src/App.tsx
|
|
@ -1,12 +1,13 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { AppShell, Loader, Center, Stack, Text } from '@mantine/core';
|
import { AppShell, Loader, Center, Stack, Text } from '@mantine/core';
|
||||||
import { DownloadScreen } from '@/screens/Download';
|
import { DownloadScreen } from '@/components/screens/Download';
|
||||||
import { LaunchScreen } from '@/screens/Launch';
|
import { LaunchScreen } from '@/components/screens/Launch';
|
||||||
import { InterfaceScreen } from '@/screens/Interface';
|
import { InterfaceScreen } from '@/components/screens/Interface';
|
||||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||||
import { AppHeader } from '@/components/AppHeader';
|
import { AppHeader } from '@/components/AppHeader';
|
||||||
|
import { UI } from '@/constants';
|
||||||
import type { UpdateInfo } from '@/types';
|
import type { UpdateInfo } from '@/types';
|
||||||
|
|
||||||
type Screen = 'download' | 'launch' | 'interface';
|
type Screen = 'download' | 'launch' | 'interface';
|
||||||
|
|
@ -167,7 +168,10 @@ export const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShell header={{ height: 60 }} padding="md">
|
<AppShell
|
||||||
|
header={{ height: UI.HEADER_HEIGHT }}
|
||||||
|
padding={currentScreen === 'interface' ? 0 : 'md'}
|
||||||
|
>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
currentScreen={currentScreen}
|
currentScreen={currentScreen}
|
||||||
activeInterfaceTab={activeInterfaceTab}
|
activeInterfaceTab={activeInterfaceTab}
|
||||||
|
|
@ -180,7 +184,7 @@ export const App = () => {
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minHeight: 'calc(100vh - 60px)',
|
minHeight: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentScreen === null ? (
|
{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';
|
} from '@mantine/core';
|
||||||
import { Download } from 'lucide-react';
|
import { Download } from 'lucide-react';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
||||||
interface DownloadCardProps {
|
interface DownloadCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -83,7 +84,7 @@ export const DownloadCard = ({
|
||||||
bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined}
|
bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<div style={{ flex: 1 }}>
|
<div className={styles.flex1}>
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Group gap="xs" align="center" mb="xs">
|
||||||
<Text fw={500} size="sm">
|
<Text fw={500} size="sm">
|
||||||
{name}
|
{name}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import { StyledTooltip } from '@/components/StyledTooltip';
|
import { StyledTooltip } from '@/components/StyledTooltip';
|
||||||
import { getPlatformDisplayName, formatFileSize } from '@/utils';
|
import { getPlatformDisplayName, formatFileSizeInMB } from '@/utils';
|
||||||
import {
|
import {
|
||||||
isAssetRecommended,
|
isAssetRecommended,
|
||||||
sortAssetsByRecommendation,
|
sortAssetsByRecommendation,
|
||||||
|
|
@ -43,7 +43,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
|
|
||||||
const regularDownloads = availableDownloads.filter((d) => d.type === 'asset');
|
const regularDownloads = availableDownloads.filter((d) => d.type === 'asset');
|
||||||
const rocmDownload = availableDownloads.find((d) => d.type === 'rocm');
|
const rocmDownload = availableDownloads.find((d) => d.type === 'rocm');
|
||||||
const latestVersion = availableDownloads[0]?.version || 'unknown';
|
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
const handleDownload = useCallback(
|
||||||
async (type: 'asset' | 'rocm', download?: DownloadItem) => {
|
async (type: 'asset' | 'rocm', download?: DownloadItem) => {
|
||||||
|
|
@ -82,7 +81,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
return (
|
return (
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
name={rocmDownload.name}
|
name={rocmDownload.name}
|
||||||
size={formatFileSize(rocmDownload.size)}
|
size={`~${formatFileSizeInMB(rocmDownload.size)}`}
|
||||||
description={getAssetDescription(rocmDownload.name)}
|
description={getAssetDescription(rocmDownload.name)}
|
||||||
version={rocmDownload.version}
|
version={rocmDownload.version}
|
||||||
isRecommended={isAssetRecommended(
|
isRecommended={isAssetRecommended(
|
||||||
|
|
@ -120,24 +119,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
<>
|
<>
|
||||||
{availableDownloads.length > 0 && (
|
{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 ? (
|
{availableDownloads.length > 0 ? (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
|
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
|
||||||
|
|
@ -186,7 +167,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
key={download.name}
|
key={download.name}
|
||||||
name={download.name}
|
name={download.name}
|
||||||
size={formatFileSize(download.size)}
|
size={formatFileSizeInMB(download.size)}
|
||||||
version={download.version}
|
version={download.version}
|
||||||
description={getAssetDescription(download.name)}
|
description={getAssetDescription(download.name)}
|
||||||
isRecommended={isAssetRecommended(
|
isRecommended={isAssetRecommended(
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Box, Text, Stack } from '@mantine/core';
|
import { Box, Text, Stack } from '@mantine/core';
|
||||||
|
import { UI } from '@/constants';
|
||||||
|
|
||||||
interface ServerTabProps {
|
interface ServerTabProps {
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
|
|
@ -46,7 +47,13 @@ export const ServerTab = ({
|
||||||
: 'KoboldAI Lite Interface';
|
: 'KoboldAI Lite Interface';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
src={iframeUrl}
|
src={iframeUrl}
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
import { UI } from '@/constants';
|
||||||
|
|
||||||
interface TerminalTabProps {
|
interface TerminalTabProps {
|
||||||
onServerReady?: (serverUrl: string) => void;
|
onServerReady?: (serverUrl: string) => void;
|
||||||
|
|
@ -104,7 +106,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
height: '80vh',
|
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
|
|
@ -119,12 +121,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
viewportRef={viewportRef}
|
viewportRef={viewportRef}
|
||||||
onScrollPositionChange={handleScroll}
|
onScrollPositionChange={handleScroll}
|
||||||
style={{
|
className={styles.terminalScrollArea}
|
||||||
flex: 1,
|
|
||||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
|
||||||
fontSize: '14px',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
}}
|
|
||||||
scrollbarSize={8}
|
scrollbarSize={8}
|
||||||
offsetScrollbars={false}
|
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 { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
||||||
interface AdvancedTabProps {
|
interface AdvancedTabProps {
|
||||||
additionalArguments: string;
|
additionalArguments: string;
|
||||||
|
|
@ -72,7 +73,7 @@ export const AdvancedTab = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="md">
|
||||||
<div>
|
<div>
|
||||||
<Group gap="xs" align="center" mb="md">
|
<Group gap="xs" align="center" mb="md">
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600}>
|
||||||
|
|
@ -81,36 +82,48 @@ export const AdvancedTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!noshift}
|
checked={!noshift}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
onNoshiftChange(!event.currentTarget.checked)
|
onNoshiftChange(!event.currentTarget.checked)
|
||||||
}
|
}
|
||||||
label="Use ContextShift"
|
label="Context Shift"
|
||||||
/>
|
/>
|
||||||
<InfoTooltip label="Use Context Shifting to reduce reprocessing." />
|
<InfoTooltip label="Use Context Shifting to reduce reprocessing." />
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</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">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={flashattention}
|
checked={flashattention}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
onFlashattentionChange(event.currentTarget.checked)
|
onFlashattentionChange(event.currentTarget.checked)
|
||||||
}
|
}
|
||||||
label="Use FlashAttention"
|
label="Flash Attention"
|
||||||
/>
|
/>
|
||||||
<InfoTooltip label="Enable flash attention for GGUF models." />
|
<InfoTooltip label="Enable flash attention to reduce memory usage. May produce incorrect answers for some prompts, but improves performance." />
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
|
||||||
|
|
||||||
{(backend === 'cuda' || backend === 'rocm') && (
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
|
||||||
<div style={{ minWidth: '200px' }}>
|
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={lowvram}
|
checked={lowvram}
|
||||||
|
|
@ -118,14 +131,21 @@ export const AdvancedTab = ({
|
||||||
onLowvramChange(event.currentTarget.checked)
|
onLowvramChange(event.currentTarget.checked)
|
||||||
}
|
}
|
||||||
label="Low VRAM"
|
label="Low VRAM"
|
||||||
|
disabled={backend !== 'cuda' && backend !== 'rocm'}
|
||||||
/>
|
/>
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
label="Avoid offloading KV Cache or scratch buffers to VRAM. Allows more layers to fit, but may result in a speed loss."
|
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.'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<div style={{ minWidth: '200px' }}>
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={quantmatmul}
|
checked={quantmatmul}
|
||||||
|
|
@ -133,12 +153,18 @@ export const AdvancedTab = ({
|
||||||
onQuantmatmulChange(event.currentTarget.checked)
|
onQuantmatmulChange(event.currentTarget.checked)
|
||||||
}
|
}
|
||||||
label="QuantMatMul"
|
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.'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<InfoTooltip label="Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing." />
|
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -150,7 +176,7 @@ export const AdvancedTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={noavx2}
|
checked={noavx2}
|
||||||
|
|
@ -170,7 +196,7 @@ export const AdvancedTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={failsafe}
|
checked={failsafe}
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import {
|
import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
TextInput,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Slider,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { File, Search } from 'lucide-react';
|
import { File, Search } from 'lucide-react';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { BackendSelector } from '@/screens/Launch/BackendSelector';
|
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
|
||||||
import { getInputValidationState } from '@/utils';
|
import { getInputValidationState } from '@/utils';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
||||||
interface GeneralTabProps {
|
interface GeneralTabProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
|
|
@ -77,7 +70,7 @@ export const GeneralTab = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="md">
|
||||||
<BackendSelector
|
<BackendSelector
|
||||||
backend={backend}
|
backend={backend}
|
||||||
onBackendChange={onBackendChange}
|
onBackendChange={onBackendChange}
|
||||||
|
|
@ -87,6 +80,10 @@ export const GeneralTab = ({
|
||||||
failsafe={failsafe}
|
failsafe={failsafe}
|
||||||
onWarningsChange={onWarningsChange}
|
onWarningsChange={onWarningsChange}
|
||||||
onBackendsReady={onBackendsReady}
|
onBackendsReady={onBackendsReady}
|
||||||
|
gpuLayers={gpuLayers}
|
||||||
|
autoGpuLayers={autoGpuLayers}
|
||||||
|
onGpuLayersChange={onGpuLayersChange}
|
||||||
|
onAutoGpuLayersChange={onAutoGpuLayersChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -94,7 +91,7 @@ export const GeneralTab = ({
|
||||||
Text Model File
|
Text Model File
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs" align="flex-start">
|
<Group gap="xs" align="flex-start">
|
||||||
<div style={{ flex: 1 }}>
|
<div className={styles.flex1}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Select a .gguf model file or enter a direct URL to file"
|
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||||
value={modelPath}
|
value={modelPath}
|
||||||
|
|
@ -126,51 +123,6 @@ export const GeneralTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Group justify="space-between" align="center" mb="xs">
|
<Group justify="space-between" align="center" mb="xs">
|
||||||
<Group gap="xs" align="center">
|
<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 { File, Search } from 'lucide-react';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils';
|
import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
||||||
interface ImageGenerationTabProps {
|
interface ImageGenerationTabProps {
|
||||||
sdmodel: string;
|
sdmodel: string;
|
||||||
|
|
@ -78,7 +79,7 @@ const ModelField = ({
|
||||||
{tooltip && <InfoTooltip label={tooltip} />}
|
{tooltip && <InfoTooltip label={tooltip} />}
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs" align="flex-start">
|
<Group gap="xs" align="flex-start">
|
||||||
<div style={{ flex: 1 }}>
|
<div className={styles.flex1}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
|
|
@ -139,7 +140,7 @@ export const ImageGenerationTab = ({
|
||||||
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="md">
|
||||||
<div>
|
<div>
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Group gap="xs" align="center" mb="xs">
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
|
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
||||||
interface NetworkTabProps {
|
interface NetworkTabProps {
|
||||||
port: number | undefined;
|
port: number | undefined;
|
||||||
|
|
@ -42,7 +43,7 @@ export const NetworkTab = ({
|
||||||
}, [port]);
|
}, [port]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="md">
|
||||||
<Group gap="lg" align="flex-start">
|
<Group gap="lg" align="flex-start">
|
||||||
<div>
|
<div>
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
|
@ -100,7 +101,7 @@ export const NetworkTab = ({
|
||||||
<div>
|
<div>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={multiuser}
|
checked={multiuser}
|
||||||
|
|
@ -113,7 +114,7 @@ export const NetworkTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={multiplayer}
|
checked={multiplayer}
|
||||||
|
|
@ -128,7 +129,7 @@ export const NetworkTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={remotetunnel}
|
checked={remotetunnel}
|
||||||
|
|
@ -141,7 +142,7 @@ export const NetworkTab = ({
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ minWidth: '200px' }}>
|
<div className={styles.minWidth200}>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={nocertify}
|
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 { useState, useEffect } from 'react';
|
||||||
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
|
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
|
||||||
import { Folder, FolderOpen } from 'lucide-react';
|
import { Folder, FolderOpen } from 'lucide-react';
|
||||||
|
import styles from '@/styles/layout.module.css';
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
export const GeneralTab = () => {
|
||||||
const [installDir, setInstallDir] = useState<string>('');
|
const [installDir, setInstallDir] = useState<string>('');
|
||||||
|
|
@ -51,7 +52,7 @@ export const GeneralTab = () => {
|
||||||
value={installDir}
|
value={installDir}
|
||||||
readOnly
|
readOnly
|
||||||
placeholder="Default installation directory"
|
placeholder="Default installation directory"
|
||||||
style={{ flex: 1 }}
|
className={styles.flex1}
|
||||||
leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export const SettingsModal = ({
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
{showVersionsTab && (
|
{showVersionsTab && (
|
||||||
<Tabs.Panel value="versions">
|
<Tabs.Panel value="versions" style={{ overflow: 'visible' }}>
|
||||||
<VersionsTab />
|
<VersionsTab />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,18 @@ import {
|
||||||
Loader,
|
Loader,
|
||||||
rem,
|
rem,
|
||||||
Center,
|
Center,
|
||||||
|
Anchor,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { RotateCcw } from 'lucide-react';
|
import { RotateCcw, ExternalLink } from 'lucide-react';
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import {
|
import {
|
||||||
getAssetDescription,
|
getAssetDescription,
|
||||||
sortAssetsByRecommendation,
|
sortAssetsByRecommendation,
|
||||||
isAssetRecommended,
|
isAssetRecommended,
|
||||||
} from '@/utils/assets';
|
} from '@/utils/assets';
|
||||||
import { getDisplayNameFromPath } from '@/utils';
|
import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils';
|
||||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||||
import type { InstalledVersion } from '@/types/electron';
|
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -50,6 +51,9 @@ export const VersionsTab = () => {
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [loadingInstalled, setLoadingInstalled] = useState(true);
|
const [loadingInstalled, setLoadingInstalled] = useState(true);
|
||||||
|
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const loadInstalledVersions = useCallback(async () => {
|
const loadInstalledVersions = useCallback(async () => {
|
||||||
setLoadingInstalled(true);
|
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(() => {
|
useEffect(() => {
|
||||||
loadInstalledVersions();
|
loadInstalledVersions();
|
||||||
}, [loadInstalledVersions]);
|
loadLatestRelease();
|
||||||
|
}, [loadInstalledVersions, loadLatestRelease]);
|
||||||
|
|
||||||
const getAllVersions = (): VersionInfo[] => {
|
const getAllVersions = (): VersionInfo[] => {
|
||||||
const versions: VersionInfo[] = [];
|
const versions: VersionInfo[] = [];
|
||||||
|
|
@ -105,7 +123,10 @@ export const VersionsTab = () => {
|
||||||
availableDownloads.forEach((download) => {
|
availableDownloads.forEach((download) => {
|
||||||
const installedVersion = installedVersions.find((v) => {
|
const installedVersion = installedVersions.find((v) => {
|
||||||
const displayName = getDisplayNameFromPath(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(
|
const isCurrent = Boolean(
|
||||||
|
|
@ -217,15 +238,41 @@ export const VersionsTab = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg" h="100%">
|
<>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center" mb="lg">
|
||||||
|
<div>
|
||||||
<Text fw={500}>Available Versions</Text>
|
<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
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadInstalledVersions();
|
loadInstalledVersions();
|
||||||
loadRemoteVersions();
|
loadRemoteVersions();
|
||||||
|
loadLatestRelease();
|
||||||
}}
|
}}
|
||||||
leftSection={
|
leftSection={
|
||||||
<RotateCcw style={{ width: rem(14), height: rem(14) }} />
|
<RotateCcw style={{ width: rem(14), height: rem(14) }} />
|
||||||
|
|
@ -235,17 +282,21 @@ export const VersionsTab = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap="xs">
|
|
||||||
{getAllVersions().map((version, index) => {
|
{getAllVersions().map((version, index) => {
|
||||||
const isDownloading = downloading === version.name;
|
const isDownloading = downloading === version.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DownloadCard
|
<div
|
||||||
key={`${version.name}-${version.version}-${index}`}
|
key={`${version.name}-${version.version}-${index}`}
|
||||||
|
style={{ paddingBottom: '8px' }}
|
||||||
|
>
|
||||||
|
<DownloadCard
|
||||||
name={version.name}
|
name={version.name}
|
||||||
size={
|
size={
|
||||||
version.size
|
version.size
|
||||||
? `${(version.size / 1024 / 1024).toFixed(1)} MB`
|
? version.isROCm
|
||||||
|
? `~${formatFileSizeInMB(version.size)}`
|
||||||
|
: formatFileSizeInMB(version.size)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
version={version.version}
|
version={version.version}
|
||||||
|
|
@ -273,6 +324,7 @@ export const VersionsTab = () => {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|
@ -283,7 +335,6 @@ export const VersionsTab = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</>
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export const DEFAULT_CONTEXT_SIZE = 4096;
|
||||||
export const DEFAULT_MODEL_URL =
|
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';
|
'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;
|
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 CONFIG_FILE_NAME = 'config.json';
|
||||||
|
|
||||||
|
export const UI = {
|
||||||
|
HEADER_HEIGHT: 60,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const { HEADER_HEIGHT } = UI;
|
||||||
|
|
||||||
export * from './defaults';
|
export * from './defaults';
|
||||||
|
|
||||||
export const GITHUB_API = {
|
export const GITHUB_API = {
|
||||||
|
|
@ -33,5 +39,5 @@ export const KOBOLDAI_URLS = {
|
||||||
export const ROCM = {
|
export const ROCM = {
|
||||||
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||||
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
|
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
|
||||||
SIZE_BYTES: 1024 * 1024 * 1024,
|
SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
DEFAULT_CONTEXT_SIZE,
|
DEFAULT_CONTEXT_SIZE,
|
||||||
DEFAULT_MODEL_URL,
|
DEFAULT_MODEL_URL,
|
||||||
DEFAULT_HOST,
|
DEFAULT_HOST,
|
||||||
DEFAULT_FLUX_MODELS,
|
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { isNotNullish } from '@/utils';
|
import { isNotNullish } from '@/utils';
|
||||||
|
|
||||||
|
|
@ -242,7 +241,7 @@ export const useLaunchConfig = () => {
|
||||||
setSdclipl('');
|
setSdclipl('');
|
||||||
setSdclipg('');
|
setSdclipg('');
|
||||||
setSdphotomaker('');
|
setSdphotomaker('');
|
||||||
setSdvae(DEFAULT_FLUX_MODELS.VAE);
|
setSdvae('');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadConfigFromFile = useCallback(
|
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 { MantineProvider } from '@mantine/core';
|
||||||
import { App } from '@/App.tsx';
|
import { App } from '@/App.tsx';
|
||||||
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
||||||
import './index.css';
|
import '@/styles/index.css';
|
||||||
|
|
||||||
const AppWithTheme = () => {
|
const AppWithTheme = () => {
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
|
|
|
||||||
|
|
@ -385,7 +385,7 @@ export class KoboldCppManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFileName = `${configName}.json`;
|
const configFileName = `${configName}.kcpps`;
|
||||||
const configPath = join(this.installDir, configFileName);
|
const configPath = join(this.installDir, configFileName);
|
||||||
|
|
||||||
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
|
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
|
||||||
|
|
@ -647,7 +647,7 @@ export class KoboldCppManager {
|
||||||
return {
|
return {
|
||||||
name: ROCM.BINARY_NAME,
|
name: ROCM.BINARY_NAME,
|
||||||
url: ROCM.DOWNLOAD_URL,
|
url: ROCM.DOWNLOAD_URL,
|
||||||
size: ROCM.SIZE_BYTES,
|
size: ROCM.SIZE_BYTES_APPROX,
|
||||||
version,
|
version,
|
||||||
type: 'rocm',
|
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;
|
name: string;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
html_url: string;
|
||||||
assets: GitHubAsset[];
|
assets: GitHubAsset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,9 @@ export const formatFileSize = (bytes: number) => {
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizeUnit;
|
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
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@npm:^8.39.1":
|
"@typescript-eslint/eslint-plugin@npm:^8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.39.1"
|
resolution: "@typescript-eslint/eslint-plugin@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||||
"@typescript-eslint/scope-manager": "npm:8.39.1"
|
"@typescript-eslint/scope-manager": "npm:8.40.0"
|
||||||
"@typescript-eslint/type-utils": "npm:8.39.1"
|
"@typescript-eslint/type-utils": "npm:8.40.0"
|
||||||
"@typescript-eslint/utils": "npm:8.39.1"
|
"@typescript-eslint/utils": "npm:8.40.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||||
graphemer: "npm:^1.4.0"
|
graphemer: "npm:^1.4.0"
|
||||||
ignore: "npm:^7.0.0"
|
ignore: "npm:^7.0.0"
|
||||||
natural-compare: "npm:^1.4.0"
|
natural-compare: "npm:^1.4.0"
|
||||||
ts-api-utils: "npm:^2.1.0"
|
ts-api-utils: "npm:^2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@typescript-eslint/parser": ^8.39.1
|
"@typescript-eslint/parser": ^8.40.0
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/7a55de558ed6ea6f09ee0b0d994b4a70e1df9f72e4afc7b3073de1b41504a36d905779304d59c34db700af60da3bb438c62480d30462a13b8b72d0b50318aeee
|
checksum: 10c0/dc8889c3255bce6956432f099059179dd13826ba29670f81ba9238ecde46764ee63459eb73a7d88f4f30e1144a2f000d79c9e3f256fa759689d9b3b74d423bda
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/parser@npm:^8.39.1":
|
"@typescript-eslint/parser@npm:^8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/parser@npm:8.39.1"
|
resolution: "@typescript-eslint/parser@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/scope-manager": "npm:8.39.1"
|
"@typescript-eslint/scope-manager": "npm:8.40.0"
|
||||||
"@typescript-eslint/types": "npm:8.39.1"
|
"@typescript-eslint/types": "npm:8.40.0"
|
||||||
"@typescript-eslint/typescript-estree": "npm:8.39.1"
|
"@typescript-eslint/typescript-estree": "npm:8.40.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/da30372c4e8dee48a0c421996bf0bf73a62a57039ee6b817eda64de2d70fdb88dd20b50615c81be7e68fd29cdd7852829b859bb8539b4a4c78030f93acaf5664
|
checksum: 10c0/43ca9589b8a1f3f4b30a214c0e2254fa0ad43458ef1258b1d62c5aad52710ad11b9315b124cda79163274147b82201a5d76fab7de413e34bfe8e377142b71e98
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/project-service@npm:8.39.1":
|
"@typescript-eslint/project-service@npm:8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/project-service@npm:8.39.1"
|
resolution: "@typescript-eslint/project-service@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/tsconfig-utils": "npm:^8.39.1"
|
"@typescript-eslint/tsconfig-utils": "npm:^8.40.0"
|
||||||
"@typescript-eslint/types": "npm:^8.39.1"
|
"@typescript-eslint/types": "npm:^8.40.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/40207af4f4e2a260ea276766d502c4736f6dc5488e84bbab6444e2786289ece2dbca2686323c48d4e9c265e409a309bf3d97d4aa03767dff8cc7642b436bda35
|
checksum: 10c0/23d62e9ada9750136d0251f268bbe1f9784442ef258bb340a2e1e866749d8076730a14749d9a320d94d7c76df2d108caf21fe35e5dc100385f04be846dc979cb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@npm:8.39.1":
|
"@typescript-eslint/scope-manager@npm:8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/scope-manager@npm:8.39.1"
|
resolution: "@typescript-eslint/scope-manager@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types": "npm:8.39.1"
|
"@typescript-eslint/types": "npm:8.40.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||||
checksum: 10c0/9466db557c1a0eaaf24b0ece5810413d11390d046bf6e47c4074879e8dba0348b835a21106c842ab20ff85f2384312cf9e20bfe7684e31640696e29957003511
|
checksum: 10c0/48af81f9cdcec466994d290561e8d2fa3f6b156a898b71dd0e65633c896543b44729c5353596e84de2ae61bfd20e1398c3309cdfe86714a9663fd5aded4c9cd0
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@npm:8.39.1, @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.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.39.1"
|
resolution: "@typescript-eslint/tsconfig-utils@npm:8.40.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/664dff0b4ae908cb98c78f9ca73c36cf57c3a2206965d9d0659649ffc02347eb30e1452499671a425592f14a2a5c5eb82ae389b34f3c415a12119506b4ebb61c
|
checksum: 10c0/c2366dcd802901d5cd4f59fc4eab7a00ed119aa4591ba59c507fe495d9af4cfca19431a603602ea675e4c861962230d1c2f100896903750cd1fcfc134702a7d0
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@npm:8.39.1":
|
"@typescript-eslint/type-utils@npm:8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/type-utils@npm:8.39.1"
|
resolution: "@typescript-eslint/type-utils@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types": "npm:8.39.1"
|
"@typescript-eslint/types": "npm:8.40.0"
|
||||||
"@typescript-eslint/typescript-estree": "npm:8.39.1"
|
"@typescript-eslint/typescript-estree": "npm:8.40.0"
|
||||||
"@typescript-eslint/utils": "npm:8.39.1"
|
"@typescript-eslint/utils": "npm:8.40.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
ts-api-utils: "npm:^2.1.0"
|
ts-api-utils: "npm:^2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/430dfefe040eae5f0c8dfbce37b5ce071095a28f335e74793923d113682e26313586e90f7bbe2c2f9bffb0da52ffdf5055ea36b96d9f218cef35aa14853122d5
|
checksum: 10c0/660b77d801b2538a4ccb65065269ad0e8370d0be985172b5ecb067f3eea22e64aa8af9e981b31bf2a34002339fe3253b09b55d181ce6d8242fc7daa80ac4aaca
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/types@npm:8.39.1, @typescript-eslint/types@npm:^8.39.1":
|
"@typescript-eslint/types@npm:8.40.0, @typescript-eslint/types@npm:^8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/types@npm:8.39.1"
|
resolution: "@typescript-eslint/types@npm:8.40.0"
|
||||||
checksum: 10c0/0e188d2d52509a24c500a87adf561387ffcac56b62cb9fd0ca1f929bb3d4eedb6b8f9d516c1890855d39930c9dd8d502d5b4600b8c9cc832d3ebb595d81c7533
|
checksum: 10c0/225374fff36d59288a5780667a7a1316c75090d5d60b70a8035ac18786120333ccd08dfdf0e05e30d5a82217e44c57b8708b769dd1eed89f12f2ac4d3a769f76
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@npm:8.39.1":
|
"@typescript-eslint/typescript-estree@npm:8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/typescript-estree@npm:8.39.1"
|
resolution: "@typescript-eslint/typescript-estree@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/project-service": "npm:8.39.1"
|
"@typescript-eslint/project-service": "npm:8.40.0"
|
||||||
"@typescript-eslint/tsconfig-utils": "npm:8.39.1"
|
"@typescript-eslint/tsconfig-utils": "npm:8.40.0"
|
||||||
"@typescript-eslint/types": "npm:8.39.1"
|
"@typescript-eslint/types": "npm:8.40.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.39.1"
|
"@typescript-eslint/visitor-keys": "npm:8.40.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
fast-glob: "npm:^3.3.2"
|
fast-glob: "npm:^3.3.2"
|
||||||
is-glob: "npm:^4.0.3"
|
is-glob: "npm:^4.0.3"
|
||||||
|
|
@ -2162,32 +2162,32 @@ __metadata:
|
||||||
ts-api-utils: "npm:^2.1.0"
|
ts-api-utils: "npm:^2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/1de1a37fed354600a08bc971492c2f14238f0a4bf07a43bedb416c17b7312d18bec92c68c8f2790bb0a1bffcd757f7962914be9f6213068f18f6c4fdde259af4
|
checksum: 10c0/6c1ffc17947cb36cbd987cf9705f85223ed1cce584b5244840e36a2b8480861f4dfdb0312f96afbc12e7d1ba586005f0d959042baa0a96a1913ac7ace8e8f6d4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/utils@npm:8.39.1":
|
"@typescript-eslint/utils@npm:8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/utils@npm:8.39.1"
|
resolution: "@typescript-eslint/utils@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
||||||
"@typescript-eslint/scope-manager": "npm:8.39.1"
|
"@typescript-eslint/scope-manager": "npm:8.40.0"
|
||||||
"@typescript-eslint/types": "npm:8.39.1"
|
"@typescript-eslint/types": "npm:8.40.0"
|
||||||
"@typescript-eslint/typescript-estree": "npm:8.39.1"
|
"@typescript-eslint/typescript-estree": "npm:8.40.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/ebc01d736af43728df9a0915058d0c771dec9cc58846ffdcbb986c78e7dabf547ea7daecd75db58b2af88a3c2a43de8a7e5f81feefacfa31be173fc384d25d77
|
checksum: 10c0/6b3858b8725083fe7db7fb9bcbde930e758a6ba8ddedd1ed27d828fc1cbe04f54b774ef9144602f8eeaafeea9b19b4fd4c46fdad52a10ade99e6b282c7d0df92
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@npm:8.39.1":
|
"@typescript-eslint/visitor-keys@npm:8.40.0":
|
||||||
version: 8.39.1
|
version: 8.40.0
|
||||||
resolution: "@typescript-eslint/visitor-keys@npm:8.39.1"
|
resolution: "@typescript-eslint/visitor-keys@npm:8.40.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types": "npm:8.39.1"
|
"@typescript-eslint/types": "npm:8.40.0"
|
||||||
eslint-visitor-keys: "npm:^4.2.1"
|
eslint-visitor-keys: "npm:^4.2.1"
|
||||||
checksum: 10c0/4d81f6826a211bc2752e25cd16d1f415f28ebc92b35142402ec23f3765f2d00963b75ac06266ad9c674ca5b057d07d8c114116e5bf14f5465dde1d1aa60bc72f
|
checksum: 10c0/592f1c8c2d3da43a7f74f8ead14f05fafc2e4609d5df36811cf92ead5dc94f6f669556a494048e4746cb3774c60bc52a8c83d75369d5e196778d935c70e7d3a1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -4431,8 +4431,8 @@ __metadata:
|
||||||
"@types/node": "npm:^24.3.0"
|
"@types/node": "npm:^24.3.0"
|
||||||
"@types/react": "npm:^19.1.10"
|
"@types/react": "npm:^19.1.10"
|
||||||
"@types/react-dom": "npm:^19.1.7"
|
"@types/react-dom": "npm:^19.1.7"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^8.39.1"
|
"@typescript-eslint/eslint-plugin": "npm:^8.40.0"
|
||||||
"@typescript-eslint/parser": "npm:^8.39.1"
|
"@typescript-eslint/parser": "npm:^8.40.0"
|
||||||
"@vitejs/plugin-react": "npm:^5.0.0"
|
"@vitejs/plugin-react": "npm:^5.0.0"
|
||||||
cross-env: "npm:^10.0.0"
|
cross-env: "npm:^10.0.0"
|
||||||
cspell: "npm:^9.2.0"
|
cspell: "npm:^9.2.0"
|
||||||
|
|
@ -4450,7 +4450,7 @@ __metadata:
|
||||||
husky: "npm:^9.1.7"
|
husky: "npm:^9.1.7"
|
||||||
jiti: "npm:^2.5.1"
|
jiti: "npm:^2.5.1"
|
||||||
lint-staged: "npm:^16.1.5"
|
lint-staged: "npm:^16.1.5"
|
||||||
lucide-react: "npm:^0.539.0"
|
lucide-react: "npm:^0.540.0"
|
||||||
prettier: "npm:^3.6.2"
|
prettier: "npm:^3.6.2"
|
||||||
react: "npm:^19.1.1"
|
react: "npm:^19.1.1"
|
||||||
react-dom: "npm:^19.1.1"
|
react-dom: "npm:^19.1.1"
|
||||||
|
|
@ -5782,12 +5782,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lucide-react@npm:^0.539.0":
|
"lucide-react@npm:^0.540.0":
|
||||||
version: 0.539.0
|
version: 0.540.0
|
||||||
resolution: "lucide-react@npm:0.539.0"
|
resolution: "lucide-react@npm:0.540.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
checksum: 10c0/ae85244dc23a78c51cc5f829ee09321cf68201333ab3d5614ed73cda77a4cd12284d3630a5a2167a39e31e02811056336de76a597c6874b28bb9984baf876494
|
checksum: 10c0/f4dc8a540b1b079958fdf7a1804dfb3f9093a9393f0c0a4b32a7e839869df5e29946c4fe226f06edd3056f6eccfa77b53e44be89f42379e7fe31c4e59caee3fa
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue