mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-04 04:04:44 -07:00
add more network settings, start detecting GPU and CPU capabilities
This commit is contained in:
parent
795bb9ec51
commit
867ced21f9
32 changed files with 1536 additions and 660 deletions
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
|
|
@ -17,3 +17,4 @@
|
||||||
- Follow the ESLint configuration (includes SonarJS and security rules)
|
- Follow the ESLint configuration (includes SonarJS and security rules)
|
||||||
- Never create tests, docs or github workflows
|
- Never create tests, docs or github workflows
|
||||||
- Stop asking me to run the "dev" script to test changes
|
- Stop asking me to run the "dev" script to test changes
|
||||||
|
- Try to move helper functions from component code to their own separate files to help minimize clutter
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@ release/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.VSCodeCounter/
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
# Prettier-specific ignores (beyond .gitignore)
|
# Prettier-specific ignores (beyond .gitignore)
|
||||||
# Keep files that should be tracked by git but not formatted by prettier
|
# Keep files that should be tracked by git but not formatted by prettier
|
||||||
|
|
||||||
# IDE/Editor config files that should exist in repo but not be formatted
|
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
# Coverage reports (if kept in repo for CI)
|
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
# Auto-generated files that might be committed
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
|
||||||
15
cspell.json
15
cspell.json
|
|
@ -22,6 +22,8 @@
|
||||||
"bundler",
|
"bundler",
|
||||||
"bundling",
|
"bundling",
|
||||||
"can",
|
"can",
|
||||||
|
"clblast",
|
||||||
|
"clinfo",
|
||||||
"classList",
|
"classList",
|
||||||
"className",
|
"className",
|
||||||
"cloneNode",
|
"cloneNode",
|
||||||
|
|
@ -70,6 +72,7 @@
|
||||||
"getDerivedStateFromError",
|
"getDerivedStateFromError",
|
||||||
"getDerivedStateFromProps",
|
"getDerivedStateFromProps",
|
||||||
"getSnapshotBeforeUpdate",
|
"getSnapshotBeforeUpdate",
|
||||||
|
"ggml",
|
||||||
"gguf",
|
"gguf",
|
||||||
"GGUF",
|
"GGUF",
|
||||||
"gif",
|
"gif",
|
||||||
|
|
@ -107,15 +110,21 @@
|
||||||
"lineheight",
|
"lineheight",
|
||||||
"linted",
|
"linted",
|
||||||
"localhost",
|
"localhost",
|
||||||
|
"lscpu",
|
||||||
|
"machdep",
|
||||||
"mainWindow",
|
"mainWindow",
|
||||||
"minified",
|
"minified",
|
||||||
"minify",
|
"minify",
|
||||||
"moz",
|
"moz",
|
||||||
|
"multiuser",
|
||||||
|
"multiplayer",
|
||||||
"namespace",
|
"namespace",
|
||||||
"namespaces",
|
"namespaces",
|
||||||
"newpackagename",
|
"newpackagename",
|
||||||
|
"nocertify",
|
||||||
"nocuda",
|
"nocuda",
|
||||||
"nodeIntegration",
|
"nodeIntegration",
|
||||||
|
"noheader",
|
||||||
"nsis",
|
"nsis",
|
||||||
"nvidia",
|
"nvidia",
|
||||||
"oldpc",
|
"oldpc",
|
||||||
|
|
@ -143,12 +152,14 @@
|
||||||
"readonly",
|
"readonly",
|
||||||
"refactor",
|
"refactor",
|
||||||
"refactoring",
|
"refactoring",
|
||||||
|
"remotetunnel",
|
||||||
"removeAttribute",
|
"removeAttribute",
|
||||||
"removeChild",
|
"removeChild",
|
||||||
"removeEventListener",
|
"removeEventListener",
|
||||||
"repo",
|
"repo",
|
||||||
"repos",
|
"repos",
|
||||||
"rocm",
|
"rocm",
|
||||||
|
"rocminfo",
|
||||||
"rollup",
|
"rollup",
|
||||||
"sass",
|
"sass",
|
||||||
"scrollbar",
|
"scrollbar",
|
||||||
|
|
@ -182,6 +193,7 @@
|
||||||
"treemap",
|
"treemap",
|
||||||
"treeshake",
|
"treeshake",
|
||||||
"treeshaking",
|
"treeshaking",
|
||||||
|
"trycloudflare",
|
||||||
"tsconfig",
|
"tsconfig",
|
||||||
"tsx",
|
"tsx",
|
||||||
"ttf",
|
"ttf",
|
||||||
|
|
@ -205,12 +217,15 @@
|
||||||
"vite",
|
"vite",
|
||||||
"vitejs",
|
"vitejs",
|
||||||
"vitest",
|
"vitest",
|
||||||
|
"vulkan",
|
||||||
|
"vulkaninfo",
|
||||||
"wayland",
|
"wayland",
|
||||||
"webContents",
|
"webContents",
|
||||||
"webkit",
|
"webkit",
|
||||||
"webp",
|
"webp",
|
||||||
"webpack",
|
"webpack",
|
||||||
"webSecurity",
|
"webSecurity",
|
||||||
|
"websearch",
|
||||||
"whitespace",
|
"whitespace",
|
||||||
"wmic",
|
"wmic",
|
||||||
"woff",
|
"woff",
|
||||||
|
|
|
||||||
37
package-lock.json
generated
37
package-lock.json
generated
|
|
@ -14,7 +14,8 @@
|
||||||
"jiti": "^2.5.1",
|
"jiti": "^2.5.1",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1",
|
||||||
|
"systeminformation": "^5.27.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cspell/eslint-plugin": "^9.2.0",
|
"@cspell/eslint-plugin": "^9.2.0",
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@types/systeminformation": "^3.23.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||||
"@typescript-eslint/parser": "^8.39.1",
|
"@typescript-eslint/parser": "^8.39.1",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
|
@ -3281,6 +3283,13 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/systeminformation": {
|
||||||
|
"version": "3.23.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.23.1.tgz",
|
||||||
|
"integrity": "sha512-0/y5m3PQLhQL7UM8MEvwFuLoTT6k5bamcZyjap12S0iHb33ngKfDDqCSnIaCaKWvRE41R/9BsZov1aE6hvcpEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/verror": {
|
"node_modules/@types/verror": {
|
||||||
"version": "1.10.11",
|
"version": "1.10.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
||||||
|
|
@ -11428,6 +11437,32 @@
|
||||||
"url": "https://opencollective.com/synckit"
|
"url": "https://opencollective.com/synckit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/systeminformation": {
|
||||||
|
"version": "5.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.7.tgz",
|
||||||
|
"integrity": "sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"os": [
|
||||||
|
"darwin",
|
||||||
|
"linux",
|
||||||
|
"win32",
|
||||||
|
"freebsd",
|
||||||
|
"openbsd",
|
||||||
|
"netbsd",
|
||||||
|
"sunos",
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"systeminformation": "lib/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "Buy me a coffee",
|
||||||
|
"url": "https://www.buymeacoffee.com/systeminfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tabbable": {
|
"node_modules/tabbable": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
|
"@types/systeminformation": "^3.23.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||||
"@typescript-eslint/parser": "^8.39.1",
|
"@typescript-eslint/parser": "^8.39.1",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
|
@ -80,7 +81,8 @@
|
||||||
"jiti": "^2.5.1",
|
"jiti": "^2.5.1",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1",
|
||||||
|
"systeminformation": "^5.27.7"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.friendly-kobold.app",
|
"appId": "com.friendly-kobold.app",
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import {
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Settings, ArrowLeft } from 'lucide-react';
|
import { Settings, ArrowLeft } from 'lucide-react';
|
||||||
import { DownloadScreen } from '@/screens/DownloadScreen';
|
import { DownloadScreen } from '@/components/screens/DownloadScreen';
|
||||||
import { LaunchScreen } from '@/screens/LaunchScreen';
|
import { LaunchScreen } from '@/components/screens/LaunchScreen';
|
||||||
import { InterfaceScreen } from '@/screens/InterfaceScreen';
|
import { InterfaceScreen } from '@/components/screens/InterfaceScreen';
|
||||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||||
import { SettingsModal } from '@/components/SettingsModal';
|
import { SettingsModal } from '@/components/SettingsModal';
|
||||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||||
|
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
import { Select, Text, Badge, Group } from '@mantine/core';
|
|
||||||
import { File } from 'lucide-react';
|
|
||||||
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
|
||||||
import type { ConfigFile } from '@/types';
|
|
||||||
|
|
||||||
interface ConfigFileSelectProps {
|
|
||||||
configFiles: ConfigFile[];
|
|
||||||
selectedFile: string | null;
|
|
||||||
loading?: boolean;
|
|
||||||
onFileSelection: (fileName: string) => 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 ConfigFileSelect = ({
|
|
||||||
configFiles,
|
|
||||||
selectedFile,
|
|
||||||
loading = false,
|
|
||||||
onFileSelection,
|
|
||||||
}: ConfigFileSelectProps) => {
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
Loading configuration files...
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configFiles.length === 0) {
|
|
||||||
return (
|
|
||||||
<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, // Clean label for selected value
|
|
||||||
extension: `.${extension}`, // Store extension separately
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label="Configuration File"
|
|
||||||
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 }) => {
|
|
||||||
// Find the original data item to get the extension
|
|
||||||
const dataItem = selectData.find((item) => item.value === option.value);
|
|
||||||
const extension = dataItem?.extension || '';
|
|
||||||
return <SelectItem label={option.label} extension={extension} />;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
44
src/components/InfoTooltip.tsx
Normal file
44
src/components/InfoTooltip.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { ActionIcon, Tooltip, useMantineColorScheme } from '@mantine/core';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
interface InfoTooltipProps {
|
||||||
|
label: string;
|
||||||
|
multiline?: boolean;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfoTooltip = ({
|
||||||
|
label,
|
||||||
|
multiline = true,
|
||||||
|
width = 300,
|
||||||
|
}: InfoTooltipProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const isDark = colorScheme === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={label}
|
||||||
|
multiline={multiline}
|
||||||
|
w={width}
|
||||||
|
withArrow
|
||||||
|
color={isDark ? 'dark' : 'gray'}
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: isDark
|
||||||
|
? 'var(--mantine-color-dark-6)'
|
||||||
|
: 'var(--mantine-color-gray-1)',
|
||||||
|
color: isDark
|
||||||
|
? 'var(--mantine-color-gray-0)'
|
||||||
|
: 'var(--mantine-color-dark-7)',
|
||||||
|
border: isDark
|
||||||
|
? '1px solid var(--mantine-color-dark-4)'
|
||||||
|
: '1px solid var(--mantine-color-gray-3)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Box, ScrollArea, Text, ActionIcon } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
ScrollArea,
|
||||||
|
Text,
|
||||||
|
ActionIcon,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from '@mantine/core';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
interface TerminalTabProps {
|
interface TerminalTabProps {
|
||||||
|
|
@ -11,6 +17,7 @@ export const TerminalTab = ({
|
||||||
onServerReady,
|
onServerReady,
|
||||||
serverOnly,
|
serverOnly,
|
||||||
}: TerminalTabProps) => {
|
}: TerminalTabProps) => {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const [terminalContent, setTerminalContent] = useState<string>('');
|
const [terminalContent, setTerminalContent] = useState<string>('');
|
||||||
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);
|
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);
|
||||||
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true);
|
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true);
|
||||||
|
|
@ -105,7 +112,10 @@ export const TerminalTab = ({
|
||||||
height: '80vh',
|
height: '80vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: 'var(--mantine-color-dark-8)',
|
backgroundColor:
|
||||||
|
colorScheme === 'dark'
|
||||||
|
? 'var(--mantine-color-dark-8)'
|
||||||
|
: 'var(--mantine-color-gray-0)',
|
||||||
borderRadius: 'inherit',
|
borderRadius: 'inherit',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
|
|
@ -137,7 +147,10 @@ export const TerminalTab = ({
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
lineHeight: 'inherit',
|
lineHeight: 'inherit',
|
||||||
color: 'var(--mantine-color-gray-0)',
|
color:
|
||||||
|
colorScheme === 'dark'
|
||||||
|
? 'var(--mantine-color-gray-0)'
|
||||||
|
: 'var(--mantine-color-dark-9)',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import {
|
import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core';
|
||||||
Stack,
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
TextInput,
|
|
||||||
ActionIcon,
|
|
||||||
Tooltip,
|
|
||||||
Switch,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Info } from 'lucide-react';
|
|
||||||
|
|
||||||
interface AdvancedTabProps {
|
interface AdvancedTabProps {
|
||||||
additionalArguments: string;
|
additionalArguments: string;
|
||||||
|
|
@ -28,24 +20,7 @@ export const AdvancedTab = ({
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
Additional arguments
|
Additional arguments
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
<InfoTooltip label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are." />
|
||||||
label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Additional command line arguments"
|
placeholder="Additional command line arguments"
|
||||||
|
|
@ -57,33 +32,14 @@ export const AdvancedTab = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Group gap="xs" align="center">
|
||||||
<Text size="sm" fw={500}>
|
<Checkbox
|
||||||
Server-only mode
|
|
||||||
</Text>
|
|
||||||
<Tooltip
|
|
||||||
label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend to interact with the LLM."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
<Switch
|
|
||||||
checked={serverOnly}
|
checked={serverOnly}
|
||||||
onChange={(event) => onServerOnlyChange(event.currentTarget.checked)}
|
onChange={(event) => onServerOnlyChange(event.currentTarget.checked)}
|
||||||
|
label="Server-only mode"
|
||||||
/>
|
/>
|
||||||
|
<InfoTooltip label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend." />
|
||||||
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
import { Stack, Text, Group, Button, ActionIcon, Menu } from '@mantine/core';
|
import {
|
||||||
import { RotateCcw, Save, Settings2 } from 'lucide-react';
|
Stack,
|
||||||
import { ConfigFileSelect } from '@/components/ConfigFileSelect';
|
Text,
|
||||||
|
Group,
|
||||||
|
Button,
|
||||||
|
ActionIcon,
|
||||||
|
Menu,
|
||||||
|
Select,
|
||||||
|
Badge,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { RotateCcw, Save, Settings2, File } from 'lucide-react';
|
||||||
|
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
interface ConfigurationManagerProps {
|
interface ConfigurationManagerProps {
|
||||||
|
|
@ -12,6 +21,39 @@ interface ConfigurationManagerProps {
|
||||||
onUpdateCurrent: () => 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 = ({
|
export const ConfigurationManager = ({
|
||||||
configFiles,
|
configFiles,
|
||||||
selectedFile,
|
selectedFile,
|
||||||
|
|
@ -43,16 +85,67 @@ export const ConfigurationManager = ({
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
<ActionIcon variant="light" onClick={onRefresh} size="lg">
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
onClick={onRefresh}
|
||||||
|
size="lg"
|
||||||
|
aria-label="Refresh configuration files"
|
||||||
|
title="Refresh configuration files"
|
||||||
|
>
|
||||||
<RotateCcw size={16} />
|
<RotateCcw size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ConfigFileSelect
|
{configFiles.length === 0 ? (
|
||||||
configFiles={configFiles}
|
<Text c="dimmed" ta="center">
|
||||||
selectedFile={selectedFile}
|
No configuration files found in the installation directory.
|
||||||
onFileSelection={onFileSelection}
|
<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>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import {
|
||||||
Group,
|
Group,
|
||||||
TextInput,
|
TextInput,
|
||||||
Button,
|
Button,
|
||||||
ActionIcon,
|
|
||||||
Tooltip,
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Slider,
|
Slider,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { File, Info, Search } from 'lucide-react';
|
import { File, Search } from 'lucide-react';
|
||||||
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
|
import { getInputValidationState } from '@/utils/validation';
|
||||||
|
|
||||||
interface GeneralTabProps {
|
interface GeneralTabProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
|
|
@ -33,20 +33,49 @@ export const GeneralTab = ({
|
||||||
onGpuLayersChange,
|
onGpuLayersChange,
|
||||||
onAutoGpuLayersChange,
|
onAutoGpuLayersChange,
|
||||||
onContextSizeChange,
|
onContextSizeChange,
|
||||||
}: GeneralTabProps) => (
|
}: 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="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm" fw={500} mb="xs">
|
<Text size="sm" fw={500} mb="xs">
|
||||||
Model File *
|
Text Model File *
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs">
|
<Group gap="xs" align="flex-start">
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Select a .gguf model file"
|
placeholder="Select a .gguf model file or enter a direct URL to file"
|
||||||
value={modelPath}
|
value={modelPath}
|
||||||
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
||||||
style={{ flex: 1 }}
|
|
||||||
required
|
required
|
||||||
|
color={getInputColor()}
|
||||||
|
error={
|
||||||
|
validationState === 'invalid' ? getHelperText() : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={onSelectModelFile}
|
onClick={onSelectModelFile}
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|
@ -74,24 +103,7 @@ export const GeneralTab = ({
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
GPU Layers
|
GPU Layers
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
<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." />
|
||||||
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."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="lg" align="center">
|
<Group gap="lg" align="center">
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
|
|
@ -103,24 +115,7 @@ export const GeneralTab = ({
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<InfoTooltip label="Automatically try to allocate the GPU layers based on available VRAM." />
|
||||||
label="Automatically try to allocate the GPU layers based on available VRAM."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={gpuLayers.toString()}
|
value={gpuLayers.toString()}
|
||||||
|
|
@ -153,24 +148,7 @@ export const GeneralTab = ({
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
Context Size
|
Context Size
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
<InfoTooltip label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory." />
|
||||||
label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={contextSize?.toString() || ''}
|
value={contextSize?.toString() || ''}
|
||||||
|
|
@ -195,3 +173,4 @@ export const GeneralTab = ({
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,62 @@
|
||||||
import {
|
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
|
||||||
Stack,
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Group,
|
|
||||||
ActionIcon,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Info } from 'lucide-react';
|
|
||||||
|
|
||||||
interface NetworkTabProps {
|
interface NetworkTabProps {
|
||||||
port: number;
|
port: number;
|
||||||
host: string;
|
host: string;
|
||||||
|
multiuser: boolean;
|
||||||
|
multiplayer: boolean;
|
||||||
|
remotetunnel: boolean;
|
||||||
|
nocertify: boolean;
|
||||||
|
websearch: boolean;
|
||||||
onPortChange: (port: number) => void;
|
onPortChange: (port: number) => void;
|
||||||
onHostChange: (host: string) => void;
|
onHostChange: (host: string) => void;
|
||||||
|
onMultiuserChange: (multiuser: boolean) => void;
|
||||||
|
onMultiplayerChange: (multiplayer: boolean) => void;
|
||||||
|
onRemotetunnelChange: (remotetunnel: boolean) => void;
|
||||||
|
onNocertifyChange: (nocertify: boolean) => void;
|
||||||
|
onWebsearchChange: (websearch: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NetworkTab = ({
|
export const NetworkTab = ({
|
||||||
port,
|
port,
|
||||||
host,
|
host,
|
||||||
|
multiuser,
|
||||||
|
multiplayer,
|
||||||
|
remotetunnel,
|
||||||
|
nocertify,
|
||||||
|
websearch,
|
||||||
onPortChange,
|
onPortChange,
|
||||||
onHostChange,
|
onHostChange,
|
||||||
|
onMultiuserChange,
|
||||||
|
onMultiplayerChange,
|
||||||
|
onRemotetunnelChange,
|
||||||
|
onNocertifyChange,
|
||||||
|
onWebsearchChange,
|
||||||
}: NetworkTabProps) => (
|
}: NetworkTabProps) => (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Port */}
|
<Group gap="lg" align="flex-start">
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Host
|
||||||
|
</Text>
|
||||||
|
<InfoTooltip label="The hostname or IP address on which KoboldCpp will bind its webserver to." />
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
placeholder="localhost"
|
||||||
|
value={host}
|
||||||
|
onChange={(event) => onHostChange(event.currentTarget.value)}
|
||||||
|
style={{ maxWidth: 200 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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}>
|
||||||
Port
|
Port
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
<InfoTooltip label="The port number on which KoboldCpp will listen for connections. Default is 5001." />
|
||||||
label="The port number on which KoboldCpp will listen for connections. Default is 5001."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="5001"
|
placeholder="5001"
|
||||||
|
|
@ -59,38 +70,81 @@ export const NetworkTab = ({
|
||||||
w={120}
|
w={120}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Host */}
|
|
||||||
<div>
|
|
||||||
<Group gap="xs" align="center" mb="xs">
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
Host
|
|
||||||
</Text>
|
|
||||||
<Tooltip
|
|
||||||
label="The hostname or IP address on which KoboldCpp will bind its webserver to."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
color="dark"
|
|
||||||
styles={{
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-6)',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
border: '1px solid var(--mantine-color-dark-4)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
|
||||||
placeholder="localhost"
|
<div>
|
||||||
value={host}
|
<Text size="sm" fw={500} mb="md">
|
||||||
onChange={(event) => onHostChange(event.currentTarget.value)}
|
Network Options
|
||||||
style={{ maxWidth: 200 }}
|
</Text>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* First row: Multiuser and Multiplayer */}
|
||||||
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
|
<div style={{ minWidth: '250px' }}>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={multiuser}
|
||||||
|
onChange={(event) =>
|
||||||
|
onMultiuserChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
label="Multiuser Mode"
|
||||||
/>
|
/>
|
||||||
|
<InfoTooltip label="Allows requests by multiple different clients to be queued and handled in sequence." />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ minWidth: '250px' }}>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={multiplayer}
|
||||||
|
onChange={(event) =>
|
||||||
|
onMultiplayerChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
label="Shared Multiplayer"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="Hosts a shared multiplayer session" />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Second row: Remote Tunnel and No Certify */}
|
||||||
|
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||||
|
<div style={{ minWidth: '250px' }}>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={remotetunnel}
|
||||||
|
onChange={(event) =>
|
||||||
|
onRemotetunnelChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
label="Remote Tunnel"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="Creates a trycloudflare tunnel. Allows you to access koboldcpp from other devices over an internet URL." />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ minWidth: '250px' }}>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={nocertify}
|
||||||
|
onChange={(event) =>
|
||||||
|
onNocertifyChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
label="No Certify Mode (Insecure)"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="Allows insecure SSL connections. Use this if you have SSL cert errors and need to bypass certificate restrictions." />
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Third row: WebSearch (single item) */}
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
checked={websearch}
|
||||||
|
onChange={(event) => onWebsearchChange(event.currentTarget.checked)}
|
||||||
|
label="Enable WebSearch"
|
||||||
|
/>
|
||||||
|
<InfoTooltip label="Enable the local search engine proxy so Web Searches can be done." />
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
isAssetRecommended,
|
isAssetRecommended,
|
||||||
sortAssetsByRecommendation,
|
sortAssetsByRecommendation,
|
||||||
} from '@/utils/assets';
|
} from '@/utils/assets';
|
||||||
import { ROCM } from '@/constants/app';
|
import { ROCM } from '@/constants';
|
||||||
import type { GitHubAsset, GitHubRelease } from '@/types';
|
import type { GitHubAsset, GitHubRelease } from '@/types';
|
||||||
|
|
||||||
interface DownloadScreenProps {
|
interface DownloadScreenProps {
|
||||||
|
|
@ -39,6 +39,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
additionalArguments,
|
additionalArguments,
|
||||||
port,
|
port,
|
||||||
host,
|
host,
|
||||||
|
multiuser,
|
||||||
|
multiplayer,
|
||||||
|
remotetunnel,
|
||||||
|
nocertify,
|
||||||
|
websearch,
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
loadConfigFromFile,
|
loadConfigFromFile,
|
||||||
|
|
@ -51,6 +56,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
handleAdditionalArgumentsChange,
|
handleAdditionalArgumentsChange,
|
||||||
handlePortChange,
|
handlePortChange,
|
||||||
handleHostChange,
|
handleHostChange,
|
||||||
|
handleMultiuserChange,
|
||||||
|
handleMultiplayerChange,
|
||||||
|
handleRemotetunnelChange,
|
||||||
|
handleNocertifyChange,
|
||||||
|
handleWebsearchChange,
|
||||||
} = useLaunchConfig();
|
} = useLaunchConfig();
|
||||||
|
|
||||||
const loadConfigFiles = useCallback(async () => {
|
const loadConfigFiles = useCallback(async () => {
|
||||||
|
|
@ -131,6 +141,31 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
|
||||||
|
handleMultiuserChange(multiuser);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiplayerChangeWithTracking = (multiplayer: boolean) => {
|
||||||
|
handleMultiplayerChange(multiplayer);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemotetunnelChangeWithTracking = (remotetunnel: boolean) => {
|
||||||
|
handleRemotetunnelChange(remotetunnel);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNocertifyChangeWithTracking = (nocertify: boolean) => {
|
||||||
|
handleNocertifyChange(nocertify);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWebsearchChangeWithTracking = (websearch: boolean) => {
|
||||||
|
handleWebsearchChange(websearch);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadConfigFiles();
|
void loadConfigFiles();
|
||||||
|
|
||||||
|
|
@ -179,6 +214,26 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
args.push('--host', 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 (additionalArguments.trim()) {
|
if (additionalArguments.trim()) {
|
||||||
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
||||||
args.push(...additionalArgs);
|
args.push(...additionalArgs);
|
||||||
|
|
@ -256,13 +311,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
<Tabs value={activeTab} onChange={setActiveTab}>
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||||
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
|
<Tabs.Tab value="image">Image Generation</Tabs.Tab>
|
||||||
<Tabs.Tab value="network">Network</Tabs.Tab>
|
<Tabs.Tab value="network">Network</Tabs.Tab>
|
||||||
<Tabs.Tab value="image" disabled>
|
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
|
||||||
Image Generation
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
|
<div style={{ minHeight: '300px' }}>
|
||||||
<Tabs.Panel value="general" pt="md">
|
<Tabs.Panel value="general" pt="md">
|
||||||
<GeneralTab
|
<GeneralTab
|
||||||
modelPath={modelPath}
|
modelPath={modelPath}
|
||||||
|
|
@ -292,8 +346,18 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
<NetworkTab
|
<NetworkTab
|
||||||
port={port}
|
port={port}
|
||||||
host={host}
|
host={host}
|
||||||
|
multiuser={multiuser}
|
||||||
|
multiplayer={multiplayer}
|
||||||
|
remotetunnel={remotetunnel}
|
||||||
|
nocertify={nocertify}
|
||||||
|
websearch={websearch}
|
||||||
onPortChange={handlePortChangeWithTracking}
|
onPortChange={handlePortChangeWithTracking}
|
||||||
onHostChange={handleHostChangeWithTracking}
|
onHostChange={handleHostChangeWithTracking}
|
||||||
|
onMultiuserChange={handleMultiuserChangeWithTracking}
|
||||||
|
onMultiplayerChange={handleMultiplayerChangeWithTracking}
|
||||||
|
onRemotetunnelChange={handleRemotetunnelChangeWithTracking}
|
||||||
|
onNocertifyChange={handleNocertifyChangeWithTracking}
|
||||||
|
onWebsearchChange={handleWebsearchChangeWithTracking}
|
||||||
/>
|
/>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
|
@ -305,6 +369,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -2,10 +2,6 @@ export const APP_NAME = 'friendly-kobold';
|
||||||
|
|
||||||
export const CONFIG_FILE_NAME = 'config.json';
|
export const CONFIG_FILE_NAME = 'config.json';
|
||||||
|
|
||||||
export const DIALOG_TITLES = {
|
|
||||||
SELECT_INSTALL_DIR: 'Select the Friendly Kobold Installation Directory',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const GITHUB_API = {
|
export const GITHUB_API = {
|
||||||
BASE_URL: 'https://api.github.com',
|
BASE_URL: 'https://api.github.com',
|
||||||
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||||
|
|
@ -10,7 +10,13 @@ export const useLaunchConfig = () => {
|
||||||
const [additionalArguments, setAdditionalArguments] = useState<string>('');
|
const [additionalArguments, setAdditionalArguments] = useState<string>('');
|
||||||
const [port, setPort] = useState<number>(5001);
|
const [port, setPort] = useState<number>(5001);
|
||||||
const [host, setHost] = useState<string>('localhost');
|
const [host, setHost] = useState<string>('localhost');
|
||||||
|
const [multiuser, setMultiuser] = useState<boolean>(false);
|
||||||
|
const [multiplayer, setMultiplayer] = useState<boolean>(false);
|
||||||
|
const [remotetunnel, setRemotetunnel] = useState<boolean>(false);
|
||||||
|
const [nocertify, setNocertify] = useState<boolean>(false);
|
||||||
|
const [websearch, setWebsearch] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||||
const configData =
|
const configData =
|
||||||
await window.electronAPI.kobold.parseConfigFile(configPath);
|
await window.electronAPI.kobold.parseConfigFile(configPath);
|
||||||
|
|
@ -42,11 +48,46 @@ export const useLaunchConfig = () => {
|
||||||
} else {
|
} else {
|
||||||
setHost('localhost');
|
setHost('localhost');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof configData.multiuser === 'number') {
|
||||||
|
setMultiuser(configData.multiuser === 1);
|
||||||
|
} else {
|
||||||
|
setMultiuser(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.multiplayer === 'boolean') {
|
||||||
|
setMultiplayer(configData.multiplayer);
|
||||||
|
} else {
|
||||||
|
setMultiplayer(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.remotetunnel === 'boolean') {
|
||||||
|
setRemotetunnel(configData.remotetunnel);
|
||||||
|
} else {
|
||||||
|
setRemotetunnel(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.nocertify === 'boolean') {
|
||||||
|
setNocertify(configData.nocertify);
|
||||||
|
} else {
|
||||||
|
setNocertify(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.websearch === 'boolean') {
|
||||||
|
setWebsearch(configData.websearch);
|
||||||
|
} else {
|
||||||
|
setWebsearch(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setGpuLayers(0);
|
setGpuLayers(0);
|
||||||
setContextSize(2048);
|
setContextSize(2048);
|
||||||
setPort(5001);
|
setPort(5001);
|
||||||
setHost('localhost');
|
setHost('localhost');
|
||||||
|
setMultiuser(false);
|
||||||
|
setMultiplayer(false);
|
||||||
|
setRemotetunnel(false);
|
||||||
|
setNocertify(false);
|
||||||
|
setWebsearch(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -59,6 +100,11 @@ export const useLaunchConfig = () => {
|
||||||
setContextSize(2048);
|
setContextSize(2048);
|
||||||
setPort(5001);
|
setPort(5001);
|
||||||
setHost('localhost');
|
setHost('localhost');
|
||||||
|
setMultiuser(false);
|
||||||
|
setMultiplayer(false);
|
||||||
|
setRemotetunnel(false);
|
||||||
|
setNocertify(false);
|
||||||
|
setWebsearch(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadConfigFromFile = useCallback(
|
const loadConfigFromFile = useCallback(
|
||||||
|
|
@ -136,6 +182,26 @@ export const useLaunchConfig = () => {
|
||||||
setHost(value);
|
setHost(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleMultiuserChange = useCallback((checked: boolean) => {
|
||||||
|
setMultiuser(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMultiplayerChange = useCallback((checked: boolean) => {
|
||||||
|
setMultiplayer(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemotetunnelChange = useCallback((checked: boolean) => {
|
||||||
|
setRemotetunnel(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleNocertifyChange = useCallback((checked: boolean) => {
|
||||||
|
setNocertify(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWebsearchChange = useCallback((checked: boolean) => {
|
||||||
|
setWebsearch(checked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverOnly,
|
serverOnly,
|
||||||
gpuLayers,
|
gpuLayers,
|
||||||
|
|
@ -145,6 +211,11 @@ export const useLaunchConfig = () => {
|
||||||
additionalArguments,
|
additionalArguments,
|
||||||
port,
|
port,
|
||||||
host,
|
host,
|
||||||
|
multiuser,
|
||||||
|
multiplayer,
|
||||||
|
remotetunnel,
|
||||||
|
nocertify,
|
||||||
|
websearch,
|
||||||
|
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
|
|
@ -158,5 +229,10 @@ export const useLaunchConfig = () => {
|
||||||
handleAdditionalArgumentsChange,
|
handleAdditionalArgumentsChange,
|
||||||
handlePortChange,
|
handlePortChange,
|
||||||
handleHostChange,
|
handleHostChange,
|
||||||
|
handleMultiuserChange,
|
||||||
|
handleMultiplayerChange,
|
||||||
|
handleRemotetunnelChange,
|
||||||
|
handleNocertifyChange,
|
||||||
|
handleWebsearchChange,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,16 @@ import { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import { GitHubService } from '@/main/services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { GPUService } from '@/main/services/GPUService';
|
import { HardwareService } from '@/main/services/HardwareService';
|
||||||
import { IPCHandlers } from '@/main/utils/IPCHandlers';
|
import { IPCHandlers } from '@/main/utils/IPCHandlers';
|
||||||
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants/app';
|
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants';
|
||||||
|
|
||||||
class FriendlyKoboldApp {
|
class FriendlyKoboldApp {
|
||||||
private windowManager: WindowManager;
|
private windowManager: WindowManager;
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private koboldManager: KoboldCppManager;
|
private koboldManager: KoboldCppManager;
|
||||||
private githubService: GitHubService;
|
private githubService: GitHubService;
|
||||||
private gpuService: GPUService;
|
private hardwareService: HardwareService;
|
||||||
private ipcHandlers: IPCHandlers;
|
private ipcHandlers: IPCHandlers;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -25,7 +25,7 @@ class FriendlyKoboldApp {
|
||||||
|
|
||||||
this.windowManager = new WindowManager(this.configManager);
|
this.windowManager = new WindowManager(this.configManager);
|
||||||
this.githubService = new GitHubService();
|
this.githubService = new GitHubService();
|
||||||
this.gpuService = new GPUService();
|
this.hardwareService = new HardwareService();
|
||||||
this.koboldManager = new KoboldCppManager(
|
this.koboldManager = new KoboldCppManager(
|
||||||
this.configManager,
|
this.configManager,
|
||||||
this.githubService,
|
this.githubService,
|
||||||
|
|
@ -35,7 +35,7 @@ class FriendlyKoboldApp {
|
||||||
this.koboldManager,
|
this.koboldManager,
|
||||||
this.configManager,
|
this.configManager,
|
||||||
this.githubService,
|
this.githubService,
|
||||||
this.gpuService
|
this.hardwareService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { dialog } from 'electron';
|
||||||
import { GitHubService } from '@/main/services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { WindowManager } from '@/main/managers/WindowManager';
|
import { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { DIALOG_TITLES, ROCM } from '@/constants/app';
|
import { ROCM } from '@/constants';
|
||||||
|
|
||||||
interface GitHubAsset {
|
interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -397,7 +397,7 @@ export class KoboldCppManager {
|
||||||
async selectInstallDirectory(): Promise<string | null> {
|
async selectInstallDirectory(): Promise<string | null> {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
title: DIALOG_TITLES.SELECT_INSTALL_DIR,
|
title: 'Select the Friendly Kobold Installation Directory',
|
||||||
defaultPath: this.installDir,
|
defaultPath: this.installDir,
|
||||||
buttonLabel: 'Select Directory',
|
buttonLabel: 'Select Directory',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
42
src/main/services/CPUService.ts
Normal file
42
src/main/services/CPUService.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import si from 'systeminformation';
|
||||||
|
|
||||||
|
export interface CPUCapabilities {
|
||||||
|
avx: boolean;
|
||||||
|
avx2: boolean;
|
||||||
|
cpuInfo: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CPUService {
|
||||||
|
async detectCPUCapabilities(): Promise<CPUCapabilities> {
|
||||||
|
try {
|
||||||
|
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
|
||||||
|
|
||||||
|
const cpuInfo: string[] = [];
|
||||||
|
if (cpu.brand) {
|
||||||
|
cpuInfo.push(cpu.brand);
|
||||||
|
}
|
||||||
|
if (cpu.cores) {
|
||||||
|
cpuInfo.push(`${cpu.cores} cores`);
|
||||||
|
}
|
||||||
|
if (cpu.speed) {
|
||||||
|
cpuInfo.push(`${cpu.speed}GHz`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avx = flags.includes('avx') || flags.includes('AVX');
|
||||||
|
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
||||||
|
|
||||||
|
return {
|
||||||
|
avx,
|
||||||
|
avx2,
|
||||||
|
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('CPU detection failed:', error);
|
||||||
|
return {
|
||||||
|
avx: false,
|
||||||
|
avx2: false,
|
||||||
|
cpuInfo: ['CPU detection failed'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,198 +1,335 @@
|
||||||
|
import si from 'systeminformation';
|
||||||
|
|
||||||
|
export interface GPUCapabilities {
|
||||||
|
cuda: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
rocm: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
vulkan: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
clblast: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class GPUService {
|
export class GPUService {
|
||||||
async detectGPU(): Promise<{
|
async detectGPU(): Promise<{
|
||||||
hasAMD: boolean;
|
hasAMD: boolean;
|
||||||
hasNVIDIA: boolean;
|
hasNVIDIA: boolean;
|
||||||
gpuInfo: string[];
|
gpuInfo: string[];
|
||||||
}> {
|
}> {
|
||||||
let gpuInfo: string[] = [];
|
try {
|
||||||
|
const graphics = await si.graphics();
|
||||||
|
|
||||||
let hasAMD = false;
|
let hasAMD = false;
|
||||||
let hasNVIDIA = false;
|
let hasNVIDIA = false;
|
||||||
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
try {
|
for (const controller of graphics.controllers) {
|
||||||
const platform = process.platform;
|
if (controller.model) {
|
||||||
const { spawn } = await import('child_process');
|
gpuInfo.push(controller.model);
|
||||||
|
}
|
||||||
|
|
||||||
if (platform === 'linux') {
|
const vendor = controller.vendor?.toLowerCase() || '';
|
||||||
try {
|
const model = controller.model?.toLowerCase() || '';
|
||||||
const lspci = spawn('lspci', ['-nn'], { timeout: 5000 });
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
lspci.stdout.on('data', (data) => {
|
|
||||||
output += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
lspci.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
const lines = output.split('\n');
|
|
||||||
const gpuLines = lines.filter(
|
|
||||||
(line) =>
|
|
||||||
line.toLowerCase().includes('vga') ||
|
|
||||||
line.toLowerCase().includes('3d') ||
|
|
||||||
line.toLowerCase().includes('display')
|
|
||||||
);
|
|
||||||
|
|
||||||
gpuInfo = gpuLines;
|
|
||||||
|
|
||||||
for (const line of gpuLines) {
|
|
||||||
const lineLower = line.toLowerCase();
|
|
||||||
if (
|
if (
|
||||||
lineLower.includes('amd') ||
|
vendor.includes('amd') ||
|
||||||
lineLower.includes('radeon') ||
|
vendor.includes('ati') ||
|
||||||
lineLower.includes('ati')
|
model.includes('radeon') ||
|
||||||
|
model.includes('amd')
|
||||||
) {
|
) {
|
||||||
hasAMD = true;
|
hasAMD = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lineLower.includes('nvidia') ||
|
vendor.includes('nvidia') ||
|
||||||
lineLower.includes('geforce') ||
|
model.includes('nvidia') ||
|
||||||
lineLower.includes('gtx') ||
|
model.includes('geforce') ||
|
||||||
lineLower.includes('rtx')
|
model.includes('gtx') ||
|
||||||
|
model.includes('rtx')
|
||||||
) {
|
) {
|
||||||
hasNVIDIA = true;
|
hasNVIDIA = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`lspci exited with code ${code}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
lspci.on('error', (error) => {
|
return {
|
||||||
reject(error);
|
hasAMD,
|
||||||
});
|
hasNVIDIA,
|
||||||
|
gpuInfo:
|
||||||
|
gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('GPU detection failed:', error);
|
||||||
|
return {
|
||||||
|
hasAMD: false,
|
||||||
|
hasNVIDIA: false,
|
||||||
|
gpuInfo: ['GPU detection failed'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
async detectGPUCapabilities(): Promise<GPUCapabilities> {
|
||||||
try {
|
const [cuda, rocm, vulkan, clblast] = await Promise.all([
|
||||||
lspci.kill('SIGTERM');
|
this.detectCUDA(),
|
||||||
} catch {
|
this.detectROCm(),
|
||||||
// Process already terminated
|
this.detectVulkan(),
|
||||||
|
this.detectCLBlast(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { cuda, rocm, vulkan, clblast };
|
||||||
}
|
}
|
||||||
reject(new Error('lspci timeout'));
|
|
||||||
}, 5000);
|
private async detectCUDA(): Promise<{
|
||||||
});
|
supported: boolean;
|
||||||
} catch {
|
devices: string[];
|
||||||
gpuInfo = ['GPU detection via lspci failed'];
|
}> {
|
||||||
}
|
|
||||||
} else if (platform === 'win32') {
|
|
||||||
try {
|
try {
|
||||||
const wmic = spawn(
|
const { spawn } = await import('child_process');
|
||||||
'wmic',
|
const nvidia = spawn(
|
||||||
['path', 'win32_VideoController', 'get', 'name'],
|
'nvidia-smi',
|
||||||
|
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
||||||
{ timeout: 5000 }
|
{ timeout: 5000 }
|
||||||
);
|
);
|
||||||
let output = '';
|
|
||||||
|
|
||||||
wmic.stdout.on('data', (data) => {
|
let output = '';
|
||||||
|
nvidia.stdout.on('data', (data) => {
|
||||||
output += data.toString();
|
output += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
wmic.on('close', (code) => {
|
nvidia.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0 && output.trim()) {
|
||||||
const lines = output
|
const devices = output
|
||||||
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line.trim() && !line.includes('Name'));
|
.map((line) => {
|
||||||
gpuInfo = lines.map((line) => line.trim());
|
const parts = line.split(',');
|
||||||
|
return parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nvidia.on('error', () => {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
nvidia.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// Process already terminated
|
||||||
|
}
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectROCm(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const rocminfo = spawn('rocminfo', [], { timeout: 5000 });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
rocminfo.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rocminfo.on('close', (code) => {
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
const devices: string[] = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line.includes('Marketing Name:')) {
|
||||||
|
const name = line.split('Marketing Name:')[1]?.trim();
|
||||||
|
if (name && !name.includes('CPU')) {
|
||||||
|
devices.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rocminfo.on('error', () => {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
rocminfo.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// Process already terminated
|
||||||
|
}
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectVulkan(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
vulkaninfo.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
vulkaninfo.on('close', (code) => {
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
const devices: string[] = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const lineLower = line.toLowerCase();
|
if (line.includes('deviceName')) {
|
||||||
if (
|
const name = line.split('=')[1]?.trim();
|
||||||
lineLower.includes('amd') ||
|
if (name) {
|
||||||
lineLower.includes('radeon') ||
|
devices.push(name);
|
||||||
lineLower.includes('ati')
|
|
||||||
) {
|
|
||||||
hasAMD = true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
lineLower.includes('nvidia') ||
|
|
||||||
lineLower.includes('geforce') ||
|
|
||||||
lineLower.includes('gtx') ||
|
|
||||||
lineLower.includes('rtx')
|
|
||||||
) {
|
|
||||||
hasNVIDIA = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve();
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`wmic exited with code ${code}`));
|
resolve({ supported: false, devices: [] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wmic.on('error', (error) => {
|
vulkaninfo.on('error', () => {
|
||||||
reject(error);
|
resolve({ supported: false, devices: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
wmic.kill('SIGTERM');
|
vulkaninfo.kill('SIGTERM');
|
||||||
} catch {
|
} catch {
|
||||||
// Process already terminated
|
// Process already terminated
|
||||||
}
|
}
|
||||||
reject(new Error('wmic timeout'));
|
resolve({ supported: false, devices: [] });
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
gpuInfo = ['GPU detection via wmic failed'];
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (platform === 'darwin') {
|
|
||||||
try {
|
|
||||||
const profiler = spawn('system_profiler', ['SPDisplaysDataType'], {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
profiler.stdout.on('data', (data) => {
|
private async detectCLBlast(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const clinfo = spawn('clinfo', ['--json'], { timeout: 5000 });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
clinfo.stdout.on('data', (data) => {
|
||||||
output += data.toString();
|
output += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
profiler.on('close', (code) => {
|
clinfo.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0 && output.trim()) {
|
||||||
gpuInfo = [output];
|
try {
|
||||||
const outputLower = output.toLowerCase();
|
const data = JSON.parse(output);
|
||||||
if (
|
const devices: string[] = [];
|
||||||
outputLower.includes('amd') ||
|
|
||||||
outputLower.includes('radeon') ||
|
if (data.platforms) {
|
||||||
outputLower.includes('ati')
|
for (const platform of data.platforms) {
|
||||||
) {
|
if (platform.devices) {
|
||||||
hasAMD = true;
|
for (const device of platform.devices) {
|
||||||
|
if (device.name && device.type !== 'CPU') {
|
||||||
|
devices.push(device.name);
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
outputLower.includes('nvidia') ||
|
|
||||||
outputLower.includes('geforce') ||
|
|
||||||
outputLower.includes('gtx') ||
|
|
||||||
outputLower.includes('rtx')
|
|
||||||
) {
|
|
||||||
hasNVIDIA = true;
|
|
||||||
}
|
}
|
||||||
resolve();
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Failed to parse JSON, but clinfo ran successfully
|
||||||
|
// Try to extract device names from text output
|
||||||
|
const lines = output.split('\n');
|
||||||
|
const devices: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('Device Name') && !line.includes('CPU')) {
|
||||||
|
const name = line.split(':')[1]?.trim();
|
||||||
|
if (name) {
|
||||||
|
devices.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`system_profiler exited with code ${code}`));
|
resolve({ supported: false, devices: [] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
profiler.on('error', (error) => {
|
clinfo.on('error', () => {
|
||||||
reject(error);
|
resolve({ supported: false, devices: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
profiler.kill('SIGTERM');
|
clinfo.kill('SIGTERM');
|
||||||
} catch {
|
} catch {
|
||||||
// Process already terminated
|
// Process already terminated
|
||||||
}
|
}
|
||||||
reject(new Error('system_profiler timeout'));
|
resolve({ supported: false, devices: [] });
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
gpuInfo = ['GPU detection via system_profiler failed'];
|
return { supported: false, devices: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
gpuInfo = ['GPU detection failed'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return { hasAMD, hasNVIDIA, gpuInfo };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { GitHubRelease } from '@/types/electron';
|
import type { GitHubRelease } from '@/types/electron';
|
||||||
import { GITHUB_API } from '@/constants/app';
|
import { GITHUB_API } from '@/constants';
|
||||||
|
|
||||||
export class GitHubService {
|
export class GitHubService {
|
||||||
private lastApiCall = 0;
|
private lastApiCall = 0;
|
||||||
|
|
|
||||||
367
src/main/services/HardwareService.ts
Normal file
367
src/main/services/HardwareService.ts
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
import si from 'systeminformation';
|
||||||
|
import type {
|
||||||
|
CPUCapabilities,
|
||||||
|
GPUCapabilities,
|
||||||
|
BasicGPUInfo,
|
||||||
|
HardwareInfo,
|
||||||
|
} from '@/types/hardware';
|
||||||
|
|
||||||
|
export class HardwareService {
|
||||||
|
async detectCPU(): Promise<CPUCapabilities> {
|
||||||
|
try {
|
||||||
|
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
|
||||||
|
|
||||||
|
const cpuInfo: string[] = [];
|
||||||
|
if (cpu.brand) {
|
||||||
|
cpuInfo.push(cpu.brand);
|
||||||
|
}
|
||||||
|
if (cpu.cores) {
|
||||||
|
cpuInfo.push(`${cpu.cores} cores`);
|
||||||
|
}
|
||||||
|
if (cpu.speed) {
|
||||||
|
cpuInfo.push(`${cpu.speed}GHz`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avx = flags.includes('avx') || flags.includes('AVX');
|
||||||
|
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
||||||
|
|
||||||
|
return {
|
||||||
|
avx,
|
||||||
|
avx2,
|
||||||
|
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('CPU detection failed:', error);
|
||||||
|
return {
|
||||||
|
avx: false,
|
||||||
|
avx2: false,
|
||||||
|
cpuInfo: ['CPU detection failed'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectGPU(): Promise<BasicGPUInfo> {
|
||||||
|
try {
|
||||||
|
const graphics = await si.graphics();
|
||||||
|
|
||||||
|
let hasAMD = false;
|
||||||
|
let hasNVIDIA = false;
|
||||||
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
|
for (const controller of graphics.controllers) {
|
||||||
|
if (controller.model) {
|
||||||
|
gpuInfo.push(controller.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vendor = controller.vendor?.toLowerCase() || '';
|
||||||
|
const model = controller.model?.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
vendor.includes('amd') ||
|
||||||
|
vendor.includes('ati') ||
|
||||||
|
model.includes('radeon') ||
|
||||||
|
model.includes('amd')
|
||||||
|
) {
|
||||||
|
hasAMD = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
vendor.includes('nvidia') ||
|
||||||
|
model.includes('nvidia') ||
|
||||||
|
model.includes('geforce') ||
|
||||||
|
model.includes('gtx') ||
|
||||||
|
model.includes('rtx')
|
||||||
|
) {
|
||||||
|
hasNVIDIA = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasAMD,
|
||||||
|
hasNVIDIA,
|
||||||
|
gpuInfo:
|
||||||
|
gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('GPU detection failed:', error);
|
||||||
|
return {
|
||||||
|
hasAMD: false,
|
||||||
|
hasNVIDIA: false,
|
||||||
|
gpuInfo: ['GPU detection failed'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectGPUCapabilities(): Promise<GPUCapabilities> {
|
||||||
|
const [cuda, rocm, vulkan, clblast] = await Promise.all([
|
||||||
|
this.detectCUDA(),
|
||||||
|
this.detectROCm(),
|
||||||
|
this.detectVulkan(),
|
||||||
|
this.detectCLBlast(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { cuda, rocm, vulkan, clblast };
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectAll(): Promise<HardwareInfo> {
|
||||||
|
const [cpu, gpu] = await Promise.all([this.detectCPU(), this.detectGPU()]);
|
||||||
|
|
||||||
|
return { cpu, gpu };
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectAllWithCapabilities(): Promise<HardwareInfo> {
|
||||||
|
const [cpu, gpu, gpuCapabilities] = await Promise.all([
|
||||||
|
this.detectCPU(),
|
||||||
|
this.detectGPU(),
|
||||||
|
this.detectGPUCapabilities(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { cpu, gpu, gpuCapabilities };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectCUDA(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const nvidia = spawn(
|
||||||
|
'nvidia-smi',
|
||||||
|
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
||||||
|
{ timeout: 5000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
nvidia.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
nvidia.on('close', (code) => {
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
const devices = output
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => {
|
||||||
|
const parts = line.split(',');
|
||||||
|
return parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nvidia.on('error', () => {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
nvidia.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// Process already terminated
|
||||||
|
}
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectROCm(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const rocminfo = spawn('rocminfo', [], { timeout: 5000 });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
rocminfo.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rocminfo.on('close', (code) => {
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
const devices: string[] = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line.includes('Marketing Name:')) {
|
||||||
|
const name = line.split('Marketing Name:')[1]?.trim();
|
||||||
|
if (name && !name.includes('CPU')) {
|
||||||
|
devices.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rocminfo.on('error', () => {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
rocminfo.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// Process already terminated
|
||||||
|
}
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectVulkan(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
vulkaninfo.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
vulkaninfo.on('close', (code) => {
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
const devices: string[] = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('deviceName')) {
|
||||||
|
const name = line.split('=')[1]?.trim();
|
||||||
|
if (name) {
|
||||||
|
devices.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
vulkaninfo.on('error', () => {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
vulkaninfo.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// Process already terminated
|
||||||
|
}
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async detectCLBlast(): Promise<{
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const clinfo = spawn('clinfo', ['--json'], { timeout: 5000 });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
clinfo.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
clinfo.on('close', (code) => {
|
||||||
|
if (code === 0 && output.trim()) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
const devices: string[] = [];
|
||||||
|
|
||||||
|
if (data.platforms) {
|
||||||
|
for (const platform of data.platforms) {
|
||||||
|
if (platform.devices) {
|
||||||
|
for (const device of platform.devices) {
|
||||||
|
if (device.name && device.type !== 'CPU') {
|
||||||
|
devices.push(device.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Failed to parse JSON, try text parsing
|
||||||
|
const lines = output.split('\n');
|
||||||
|
const devices: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('Device Name') && !line.includes('CPU')) {
|
||||||
|
const name = line.split(':')[1]?.trim();
|
||||||
|
if (name) {
|
||||||
|
devices.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
supported: devices.length > 0,
|
||||||
|
devices,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clinfo.on('error', () => {
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
clinfo.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// Process already terminated
|
||||||
|
}
|
||||||
|
resolve({ supported: false, devices: [] });
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { supported: false, devices: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,24 +3,24 @@ import { shell, app } from 'electron';
|
||||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { GitHubService } from '@/main/services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { GPUService } from '@/main/services/GPUService';
|
import { HardwareService } from '@/main/services/HardwareService';
|
||||||
|
|
||||||
export class IPCHandlers {
|
export class IPCHandlers {
|
||||||
private koboldManager: KoboldCppManager;
|
private koboldManager: KoboldCppManager;
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private githubService: GitHubService;
|
private githubService: GitHubService;
|
||||||
private gpuService: GPUService;
|
private hardwareService: HardwareService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
koboldManager: KoboldCppManager,
|
koboldManager: KoboldCppManager,
|
||||||
configManager: ConfigManager,
|
configManager: ConfigManager,
|
||||||
githubService: GitHubService,
|
githubService: GitHubService,
|
||||||
gpuService: GPUService
|
hardwareService: HardwareService
|
||||||
) {
|
) {
|
||||||
this.koboldManager = koboldManager;
|
this.koboldManager = koboldManager;
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
this.githubService = githubService;
|
this.githubService = githubService;
|
||||||
this.gpuService = gpuService;
|
this.hardwareService = hardwareService;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupHandlers() {
|
setupHandlers() {
|
||||||
|
|
@ -104,7 +104,21 @@ export class IPCHandlers {
|
||||||
this.koboldManager.selectInstallDirectory()
|
this.koboldManager.selectInstallDirectory()
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectGPU', () => this.gpuService.detectGPU());
|
ipcMain.handle('kobold:detectGPU', () => this.hardwareService.detectGPU());
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:detectCPU', () => this.hardwareService.detectCPU());
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:detectGPUCapabilities', () =>
|
||||||
|
this.hardwareService.detectGPUCapabilities()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:detectHardware', () =>
|
||||||
|
this.hardwareService.detectAll()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:detectAllCapabilities', () =>
|
||||||
|
this.hardwareService.detectAllWithCapabilities()
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getPlatform', () => ({
|
ipcMain.handle('kobold:getPlatform', () => ({
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ const koboldAPI: KoboldAPI = {
|
||||||
getAllReleases: () => ipcRenderer.invoke('kobold:getAllReleases'),
|
getAllReleases: () => ipcRenderer.invoke('kobold:getAllReleases'),
|
||||||
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
|
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
|
||||||
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
|
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
|
||||||
|
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
|
||||||
|
detectGPUCapabilities: () =>
|
||||||
|
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
||||||
|
detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'),
|
||||||
|
detectAllCapabilities: () =>
|
||||||
|
ipcRenderer.invoke('kobold:detectAllCapabilities'),
|
||||||
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
||||||
selectInstallDirectory: () =>
|
selectInstallDirectory: () =>
|
||||||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||||
|
|
|
||||||
21
src/types/electron.d.ts
vendored
21
src/types/electron.d.ts
vendored
|
|
@ -1,3 +1,12 @@
|
||||||
|
import type {
|
||||||
|
CPUCapabilities,
|
||||||
|
GPUCapabilities,
|
||||||
|
BasicGPUInfo,
|
||||||
|
HardwareInfo,
|
||||||
|
PlatformInfo,
|
||||||
|
SystemCapabilities,
|
||||||
|
} from '@/types/hardware';
|
||||||
|
|
||||||
interface GitHubAsset {
|
interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
browser_download_url: string;
|
browser_download_url: string;
|
||||||
|
|
@ -53,12 +62,12 @@ export interface KoboldAPI {
|
||||||
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
|
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
|
||||||
getLatestRelease: () => Promise<GitHubRelease>;
|
getLatestRelease: () => Promise<GitHubRelease>;
|
||||||
getAllReleases: () => Promise<GitHubRelease[]>;
|
getAllReleases: () => Promise<GitHubRelease[]>;
|
||||||
getPlatform: () => Promise<{ platform: string; arch: string }>;
|
getPlatform: () => Promise<PlatformInfo>;
|
||||||
detectGPU: () => Promise<{
|
detectGPU: () => Promise<BasicGPUInfo>;
|
||||||
hasAMD: boolean;
|
detectCPU: () => Promise<CPUCapabilities>;
|
||||||
hasNVIDIA: boolean;
|
detectGPUCapabilities: () => Promise<GPUCapabilities>;
|
||||||
gpuInfo: string[];
|
detectHardware: () => Promise<HardwareInfo>;
|
||||||
}>;
|
detectAllCapabilities: () => Promise<HardwareInfo>;
|
||||||
getCurrentInstallDir: () => Promise<string>;
|
getCurrentInstallDir: () => Promise<string>;
|
||||||
selectInstallDirectory: () => Promise<string | null>;
|
selectInstallDirectory: () => Promise<string | null>;
|
||||||
downloadRelease: (
|
downloadRelease: (
|
||||||
|
|
|
||||||
46
src/types/hardware.ts
Normal file
46
src/types/hardware.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
export interface CPUCapabilities {
|
||||||
|
avx: boolean;
|
||||||
|
avx2: boolean;
|
||||||
|
cpuInfo: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GPUCapabilities {
|
||||||
|
cuda: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
rocm: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
vulkan: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
clblast: {
|
||||||
|
supported: boolean;
|
||||||
|
devices: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasicGPUInfo {
|
||||||
|
hasAMD: boolean;
|
||||||
|
hasNVIDIA: boolean;
|
||||||
|
gpuInfo: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HardwareInfo {
|
||||||
|
cpu: CPUCapabilities;
|
||||||
|
gpu: BasicGPUInfo;
|
||||||
|
gpuCapabilities?: GPUCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlatformInfo {
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemCapabilities {
|
||||||
|
hardware: HardwareInfo;
|
||||||
|
platform: PlatformInfo;
|
||||||
|
}
|
||||||
|
|
@ -46,3 +46,12 @@ export interface ROCmDownload {
|
||||||
url: string;
|
url: string;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
CPUCapabilities,
|
||||||
|
GPUCapabilities,
|
||||||
|
BasicGPUInfo,
|
||||||
|
HardwareInfo,
|
||||||
|
PlatformInfo,
|
||||||
|
SystemCapabilities,
|
||||||
|
} from './hardware';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ASSET_SUFFIXES } from '@/constants/app';
|
import { ASSET_SUFFIXES } from '@/constants';
|
||||||
|
|
||||||
export const getAssetDescription = (assetName: string): string => {
|
export const getAssetDescription = (assetName: string): string => {
|
||||||
const name = assetName.toLowerCase();
|
const name = assetName.toLowerCase();
|
||||||
|
|
|
||||||
31
src/utils/validation.ts
Normal file
31
src/utils/validation.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export const isValidUrl = (string: string): boolean => {
|
||||||
|
try {
|
||||||
|
const url = new URL(string);
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidFilePath = (path: string): boolean => {
|
||||||
|
if (!path.trim()) return false;
|
||||||
|
|
||||||
|
const validExtensions = ['.gguf'];
|
||||||
|
const hasValidExtension = validExtensions.some((ext) =>
|
||||||
|
path.toLowerCase().endsWith(ext)
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasValidExtension || path.includes('/') || path.includes('\\');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInputValidationState = (
|
||||||
|
path: string
|
||||||
|
): 'valid' | 'invalid' | 'neutral' => {
|
||||||
|
if (!path.trim()) return 'neutral';
|
||||||
|
|
||||||
|
if (isValidUrl(path) || isValidFilePath(path)) {
|
||||||
|
return 'valid';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'invalid';
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue