mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
tool migrations: biome -> oxc, yarn -> pnpm
This commit is contained in:
parent
905edb2050
commit
1bead396d1
132 changed files with 8598 additions and 8457 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -8,6 +8,5 @@ release/
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.VSCodeCounter/
|
.VSCodeCounter/
|
||||||
|
|
||||||
# Yarn
|
# pnpm
|
||||||
.yarn/
|
pnpm-debug.log*
|
||||||
.pnp.*
|
|
||||||
|
|
|
||||||
7
.oxfmtrc.json
Normal file
7
.oxfmtrc.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"singleQuote": true,
|
||||||
|
"sortImports": {
|
||||||
|
"groups": ["builtin", "external", "internal", ["parent", "sibling", "index"], "style"]
|
||||||
|
}
|
||||||
|
}
|
||||||
67
.oxlintrc.json
Normal file
67
.oxlintrc.json
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"options": {
|
||||||
|
"typeAware": true,
|
||||||
|
"typeCheck": true
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error",
|
||||||
|
"suspicious": "warn",
|
||||||
|
"perf": "warn",
|
||||||
|
"style": "warn"
|
||||||
|
},
|
||||||
|
"plugins": ["typescript", "react", "react-perf", "import", "unicorn", "oxc"],
|
||||||
|
"rules": {
|
||||||
|
"import/no-cycle": ["error", { "maxDepth": 3 }],
|
||||||
|
"import/no-absolute-path": "off",
|
||||||
|
"import/no-unassigned-import": "off",
|
||||||
|
"import/no-named-export": "off",
|
||||||
|
"import/group-exports": "off",
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"import/exports-last": "off",
|
||||||
|
"import/no-nodejs-modules": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/iframe-missing-sandbox": "off",
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/jsx-no-target-blank": "error",
|
||||||
|
"react-perf/jsx-no-new-function-as-prop": "off",
|
||||||
|
"react-perf/jsx-no-new-object-as-prop": "off",
|
||||||
|
"react-perf/jsx-no-jsx-as-prop": "off",
|
||||||
|
"react-perf/jsx-no-new-array-as-prop": "off",
|
||||||
|
"react/jsx-max-depth": "off",
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
"react/jsx-handler-names": "off",
|
||||||
|
"react/no-array-index-key": "off",
|
||||||
|
"typescript/no-floating-promises": "error",
|
||||||
|
"typescript/no-misused-promises": "error",
|
||||||
|
"typescript/no-unsafe-type-assertion": "off",
|
||||||
|
"typescript/ban-ts-comment": "warn",
|
||||||
|
"typescript/no-deprecated": "warn",
|
||||||
|
"typescript/prefer-nullish-coalescing": "warn",
|
||||||
|
"typescript/require-await": "warn",
|
||||||
|
"typescript/return-await": "warn",
|
||||||
|
"unicorn/no-null": "off",
|
||||||
|
"unicorn/prefer-global-this": "off",
|
||||||
|
"unicorn/filename-case": "off",
|
||||||
|
"unicorn/no-nested-ternary": "off",
|
||||||
|
"unicorn/catch-error-name": "off",
|
||||||
|
"unicorn/prefer-ternary": "off",
|
||||||
|
"no-ternary": "off",
|
||||||
|
"no-nested-ternary": "off",
|
||||||
|
"no-magic-numbers": "off",
|
||||||
|
"id-length": "off",
|
||||||
|
"func-style": "off",
|
||||||
|
"sort-imports": "off",
|
||||||
|
"sort-keys": "off",
|
||||||
|
"max-statements": "off",
|
||||||
|
"max-params": "off",
|
||||||
|
"prefer-destructuring": "off",
|
||||||
|
"no-continue": "off",
|
||||||
|
"init-declarations": "off",
|
||||||
|
"curly": "off",
|
||||||
|
"no-implicit-coercion": "off",
|
||||||
|
"no-duplicate-imports": "off",
|
||||||
|
"no-await-in-loop": "off"
|
||||||
|
},
|
||||||
|
"ignorePatterns": ["*.config.*", "scripts/**"]
|
||||||
|
}
|
||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": ["biomejs.biome"]
|
"recommendations": ["oxc.oxc-vscode"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
|
|
@ -1,25 +1,4 @@
|
||||||
{
|
{
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.biome": "explicit"
|
|
||||||
},
|
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[javascriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
nodeLinker: node-modules
|
|
||||||
42
biome.json
42
biome.json
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"indentStyle": "space",
|
|
||||||
"lineWidth": 100
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"suspicious": {
|
|
||||||
"noArrayIndexKey": "off"
|
|
||||||
},
|
|
||||||
"a11y": {
|
|
||||||
"noSvgWithoutTitle": "off",
|
|
||||||
"useAltText": "off"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"noDangerouslySetInnerHtml": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"semicolons": "always",
|
|
||||||
"trailingCommas": "es5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"includes": ["*.config.*", "vite.config.*", "scripts/**"],
|
|
||||||
"linter": {
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig } from 'electron-vite';
|
import { defineConfig } from 'electron-vite';
|
||||||
import { resolve } from 'path';
|
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
|
||||||
54
package.json
54
package.json
|
|
@ -1,38 +1,33 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"version": "1.20.4",
|
||||||
"version": "1.20.3",
|
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
|
||||||
"homepage": "./",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=24.0.0"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@4.13.0",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "electron-vite dev",
|
|
||||||
"build": "electron-vite build",
|
|
||||||
"package": "electron-vite build && electron-builder --publish=never",
|
|
||||||
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
|
|
||||||
"check": "biome check . && tsc --noEmit",
|
|
||||||
"fix": "biome check --write .",
|
|
||||||
"release": "yarn dlx tsx scripts/release.ts"
|
|
||||||
},
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"llm",
|
|
||||||
"ai",
|
"ai",
|
||||||
"local-ai",
|
|
||||||
"language-model",
|
|
||||||
"koboldcpp",
|
|
||||||
"electron",
|
"electron",
|
||||||
"privacy",
|
"koboldcpp",
|
||||||
"offline"
|
"language-model",
|
||||||
|
"llm",
|
||||||
|
"local-ai",
|
||||||
|
"offline",
|
||||||
|
"privacy"
|
||||||
],
|
],
|
||||||
|
"homepage": "./",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "lone-cloud",
|
"name": "lone-cloud",
|
||||||
"email": "lonecloud604@proton.me"
|
"email": "lonecloud604@proton.me"
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0-or-later",
|
"main": "out/main/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"build": "electron-vite build",
|
||||||
|
"package": "electron-vite build && electron-builder --publish=never",
|
||||||
|
"analyze": "cross-env ANALYZE=true electron-vite build && pnpm dlx open-cli dist/stats.html",
|
||||||
|
"check": "oxlint && oxfmt --check",
|
||||||
|
"fix": "oxlint --fix && oxfmt",
|
||||||
|
"release": "node --no-warnings scripts/release.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/search": "^6.6.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
|
@ -60,7 +55,6 @@
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.7",
|
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
|
|
@ -72,10 +66,17 @@
|
||||||
"electron-builder": "^26.8.1",
|
"electron-builder": "^26.8.1",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
|
"oxfmt": "^0.40.0",
|
||||||
|
"oxlint": "^1.55.0",
|
||||||
|
"oxlint-tsgolint": "^0.17.0",
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^8.0.0"
|
"vite": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.32.1",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.gerbil.app",
|
"appId": "com.gerbil.app",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
|
|
@ -182,5 +183,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"productName": "Gerbil"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6453
pnpm-lock.yaml
generated
Normal file
6453
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
allowBuilds:
|
||||||
|
electron: true
|
||||||
|
electron-winstaller: true
|
||||||
|
esbuild: true
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
interface PackageJson {
|
interface PackageJson {
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -10,7 +9,7 @@ interface PackageJson {
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageJson: PackageJson = JSON.parse(
|
const packageJson: PackageJson = JSON.parse(
|
||||||
readFileSync(join(__dirname, '..', 'package.json'), 'utf8')
|
readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
|
||||||
);
|
);
|
||||||
const currentVersion = packageJson.version;
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
|
@ -22,7 +21,7 @@ try {
|
||||||
const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' });
|
const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' });
|
||||||
if (gitStatus.trim() !== '') {
|
if (gitStatus.trim() !== '') {
|
||||||
console.error(
|
console.error(
|
||||||
'Error: There are uncommitted changes. Please commit or stash them before creating a release.'
|
'Error: There are uncommitted changes. Please commit or stash them before creating a release.',
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import type { KoboldCrashInfo } from '@/types/ipc';
|
import type { KoboldCrashInfo } from '@/types/ipc';
|
||||||
|
|
||||||
|
|
@ -15,15 +16,15 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
|
||||||
|
|
||||||
if (crashInfo.signal) {
|
if (crashInfo.signal) {
|
||||||
const signalDescriptions: Record<string, string> = {
|
const signalDescriptions: Record<string, string> = {
|
||||||
SIGKILL: 'The process was forcefully terminated',
|
|
||||||
SIGSEGV: 'Memory access violation (segmentation fault)',
|
|
||||||
SIGABRT: 'The process aborted unexpectedly',
|
SIGABRT: 'The process aborted unexpectedly',
|
||||||
SIGBUS: 'Bus error (invalid memory access)',
|
SIGBUS: 'Bus error (invalid memory access)',
|
||||||
SIGFPE: 'Floating-point exception',
|
SIGFPE: 'Floating-point exception',
|
||||||
SIGILL: 'Illegal instruction',
|
|
||||||
SIGTERM: 'The process was terminated',
|
|
||||||
SIGSTOP: 'The process was stopped',
|
|
||||||
SIGHUP: 'The process lost its controlling terminal',
|
SIGHUP: 'The process lost its controlling terminal',
|
||||||
|
SIGILL: 'Illegal instruction',
|
||||||
|
SIGKILL: 'The process was forcefully terminated',
|
||||||
|
SIGSEGV: 'Memory access violation (segmentation fault)',
|
||||||
|
SIGSTOP: 'The process was stopped',
|
||||||
|
SIGTERM: 'The process was terminated',
|
||||||
};
|
};
|
||||||
|
|
||||||
return signalDescriptions[crashInfo.signal] || `Terminated by signal ${crashInfo.signal}`;
|
return signalDescriptions[crashInfo.signal] || `Terminated by signal ${crashInfo.signal}`;
|
||||||
|
|
@ -37,7 +38,9 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BackendCrashModal = ({ opened, onClose, crashInfo }: BackendCrashModalProps) => {
|
export const BackendCrashModal = ({ opened, onClose, crashInfo }: BackendCrashModalProps) => {
|
||||||
if (!crashInfo) return null;
|
if (!crashInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const description = getCrashDescription(crashInfo);
|
const description = getCrashDescription(crashInfo);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Button, Checkbox, Group, Stack, Text } from '@mantine/core';
|
import { Button, Checkbox, Group, Stack, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface EjectConfirmModalProps {
|
interface EjectConfirmModalProps {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Alert, Button, Center, rem, Stack, Text } from '@mantine/core';
|
import { Alert, Button, Center, Stack, Text, rem } from '@mantine/core';
|
||||||
import { AlertTriangle, FolderOpen } from 'lucide-react';
|
import { AlertTriangle, FolderOpen } from 'lucide-react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
@ -35,7 +35,7 @@ const ErrorFallback = ({
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<FolderOpen style={{ height: rem(16), width: rem(16) }} />}
|
||||||
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
||||||
>
|
>
|
||||||
Show Logs Folder
|
Show Logs Folder
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,20 @@ interface PerformanceBadgeProps {
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePerformanceClick = async () => {
|
||||||
|
const result = await window.electronAPI.app.openPerformanceManager();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
window.electronAPI.logs.logError(`Failed to open performance manager: ${result.error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const PerformanceBadge = ({
|
export const PerformanceBadge = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
iconOnly = false,
|
iconOnly = false,
|
||||||
}: PerformanceBadgeProps) => {
|
}: PerformanceBadgeProps) => {
|
||||||
const handlePerformanceClick = async () => {
|
|
||||||
const result = await window.electronAPI.app.openPerformanceManager();
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
window.electronAPI.logs.logError(`Failed to open performance manager: ${result.error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (iconOnly) {
|
if (iconOnly) {
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltipLabel} position="top">
|
<Tooltip label={tooltipLabel} position="top">
|
||||||
|
|
@ -38,14 +38,14 @@ export const PerformanceBadge = ({
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="light"
|
variant="light"
|
||||||
style={{
|
style={{
|
||||||
minWidth: '5rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
height: 'auto',
|
|
||||||
padding: '0.25rem 0.5rem',
|
|
||||||
margin: '0.125rem 0',
|
|
||||||
borderRadius: '0.75rem',
|
borderRadius: '0.75rem',
|
||||||
fontSize: '0.7em',
|
fontSize: '0.7em',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
height: 'auto',
|
||||||
|
margin: '0.125rem 0',
|
||||||
|
minWidth: '5rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
onClick={() => void handlePerformanceClick()}
|
onClick={() => void handlePerformanceClick()}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { ActionIcon, AppShell, CopyButton, Group, Tooltip } from '@mantine/core';
|
import { ActionIcon, AppShell, CopyButton, Group, Tooltip } from '@mantine/core';
|
||||||
import { Check, Globe, NotepadText } from 'lucide-react';
|
import { Check, Globe, NotepadText } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
|
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { getTunnelInterfaceUrl } from '@/utils/interface';
|
import { getTunnelInterfaceUrl } from '@/utils/interface';
|
||||||
|
|
||||||
import { PerformanceBadge } from './PerformanceBadge';
|
import { PerformanceBadge } from './PerformanceBadge';
|
||||||
|
|
||||||
export const StatusBar = () => {
|
export const StatusBar = () => {
|
||||||
|
|
@ -23,7 +25,9 @@ export const StatusBar = () => {
|
||||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||||
|
|
||||||
const tunnelUrl = useMemo(() => {
|
const tunnelUrl = useMemo(() => {
|
||||||
if (!tunnelBaseUrl) return null;
|
if (!tunnelBaseUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') {
|
if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') {
|
||||||
return tunnelBaseUrl;
|
return tunnelBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
@ -42,17 +46,23 @@ export const StatusBar = () => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
const handleCpuMetrics = (metrics: CpuMetrics) => {
|
const handleCpuMetrics = (metrics: CpuMetrics) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setCpuMetrics(metrics);
|
setCpuMetrics(metrics);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemoryMetrics = (metrics: MemoryMetrics) => {
|
const handleMemoryMetrics = (metrics: MemoryMetrics) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setMemoryMetrics(metrics);
|
setMemoryMetrics(metrics);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGpuMetrics = (metrics: GpuMetrics) => {
|
const handleGpuMetrics = (metrics: GpuMetrics) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setGpuMetrics(metrics);
|
setGpuMetrics(metrics);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import icon from '/icon.png';
|
||||||
import { ActionIcon, AppShell, Box, Group, Image, Tooltip } from '@mantine/core';
|
import { ActionIcon, AppShell, Box, Group, Image, Tooltip } from '@mantine/core';
|
||||||
import { Copy, Minus, Settings, Square, X } from 'lucide-react';
|
import { Copy, Minus, Settings, Square, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { UpdateButton } from '@/components/App/UpdateButton';
|
import { UpdateButton } from '@/components/App/UpdateButton';
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||||
|
|
@ -10,7 +12,6 @@ import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import type { InterfaceTab, Screen, SelectOption } from '@/types';
|
import type { InterfaceTab, Screen, SelectOption } from '@/types';
|
||||||
import { getAvailableInterfaceOptions } from '@/utils/interface';
|
import { getAvailableInterfaceOptions } from '@/utils/interface';
|
||||||
import icon from '/icon.png';
|
|
||||||
|
|
||||||
interface TitleBarProps {
|
interface TitleBarProps {
|
||||||
currentScreen: Screen;
|
currentScreen: Screen;
|
||||||
|
|
@ -19,6 +20,18 @@ interface TitleBarProps {
|
||||||
onTabChange: (tab: InterfaceTab) => void;
|
onTabChange: (tab: InterfaceTab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderOption = ({ option }: { option: SelectOption }) => (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
color: option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
|
||||||
|
fontWeight: option.value === 'eject' ? 600 : undefined,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
|
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
|
||||||
const {
|
const {
|
||||||
resolvedColorScheme: colorScheme,
|
resolvedColorScheme: colorScheme,
|
||||||
|
|
@ -40,18 +53,6 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderOption = ({ option }: { option: SelectOption }) => (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
color: option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
|
|
||||||
fontWeight: option.value === 'eject' ? 600 : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeState = async () => {
|
const initializeState = async () => {
|
||||||
const currentMaximizedState = await window.electronAPI.app.isMaximized();
|
const currentMaximizedState = await window.electronAPI.app.isMaximized();
|
||||||
|
|
@ -61,27 +62,27 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
void initializeState();
|
void initializeState();
|
||||||
|
|
||||||
const cleanup = window.electronAPI.app.onWindowStateToggle(() =>
|
const cleanup = window.electronAPI.app.onWindowStateToggle(() =>
|
||||||
setIsMaximized((prev) => !prev)
|
setIsMaximized((prev) => !prev),
|
||||||
);
|
);
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell.Header style={{ display: 'flex', flexDirection: 'column', border: 'none' }}>
|
<AppShell.Header style={{ border: 'none', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
height: TITLEBAR_HEIGHT,
|
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
||||||
padding: '0.125rem 0 0.125rem 0.5rem',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
|
colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
border: '1px solid var(--mantine-color-default-border)',
|
||||||
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
display: 'flex',
|
||||||
userSelect: 'none',
|
height: TITLEBAR_HEIGHT,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '0.125rem 0 0.125rem 0.5rem',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}>
|
<Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}>
|
||||||
|
|
@ -97,10 +98,10 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
WebkitAppRegion: 'no-drag',
|
WebkitAppRegion: 'no-drag',
|
||||||
|
left: '50%',
|
||||||
|
position: 'absolute',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentScreen === 'interface' && (
|
{currentScreen === 'interface' && (
|
||||||
|
|
@ -113,19 +114,19 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
data={getAvailableInterfaceOptions({
|
data={getAvailableInterfaceOptions({
|
||||||
frontendPreference,
|
frontendPreference,
|
||||||
imageGenerationFrontendPreference,
|
imageGenerationFrontendPreference,
|
||||||
isTextMode,
|
|
||||||
isImageGenerationMode,
|
isImageGenerationMode,
|
||||||
|
isTextMode,
|
||||||
})}
|
})}
|
||||||
renderOption={renderOption}
|
renderOption={renderOption}
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
style={{ textAlign: 'center', minWidth: '7.5rem' }}
|
style={{ minWidth: '7.5rem', textAlign: 'center' }}
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
userSelect: 'none',
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center',
|
||||||
|
userSelect: 'none',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -152,34 +153,34 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
width: '0.1rem',
|
|
||||||
height: '1.25rem',
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
colorScheme === 'dark'
|
colorScheme === 'dark'
|
||||||
? 'var(--mantine-color-dark-3)'
|
? 'var(--mantine-color-dark-3)'
|
||||||
: 'var(--mantine-color-gray-4)',
|
: 'var(--mantine-color-gray-4)',
|
||||||
|
height: '1.25rem',
|
||||||
margin: '0 0.25rem',
|
margin: '0 0.25rem',
|
||||||
|
width: '0.1rem',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
|
color: undefined,
|
||||||
icon: <Minus size="1rem" />,
|
icon: <Minus size="1rem" />,
|
||||||
onClick: () => void window.electronAPI.app.minimizeWindow(),
|
|
||||||
color: undefined,
|
|
||||||
label: 'Minimize window',
|
label: 'Minimize window',
|
||||||
|
onClick: () => void window.electronAPI.app.minimizeWindow(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
|
|
||||||
onClick: () => void window.electronAPI.app.maximizeWindow(),
|
|
||||||
color: undefined,
|
color: undefined,
|
||||||
|
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
|
||||||
label: isMaximized ? 'Restore window' : 'Maximize window',
|
label: isMaximized ? 'Restore window' : 'Maximize window',
|
||||||
|
onClick: () => void window.electronAPI.app.maximizeWindow(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <X size="1.25rem" />,
|
|
||||||
onClick: () => void window.electronAPI.app.closeWindow(),
|
|
||||||
color: 'red' as const,
|
color: 'red' as const,
|
||||||
|
icon: <X size="1.25rem" />,
|
||||||
label: 'Close window',
|
label: 'Close window',
|
||||||
|
onClick: () => void window.electronAPI.app.closeWindow(),
|
||||||
},
|
},
|
||||||
].map((button, index) => (
|
].map((button, index) => (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Anchor, Button, Card, Group, Loader, Progress, Stack, Text } from '@mantine/core';
|
import { Anchor, Button, Card, Group, Loader, Progress, Stack, Text } from '@mantine/core';
|
||||||
import { Download, ExternalLink, X } from 'lucide-react';
|
import { Download, ExternalLink, X } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { GITHUB_API } from '@/constants';
|
import { GITHUB_API } from '@/constants';
|
||||||
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
|
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
|
||||||
|
|
@ -30,7 +31,7 @@ export const UpdateAvailableModal = ({
|
||||||
const availableUpdate = updateInfo?.availableUpdate;
|
const availableUpdate = updateInfo?.availableUpdate;
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
const isDownloading = !!availableUpdate && downloading === availableUpdate.name;
|
const isDownloading = Boolean(availableUpdate) && downloading === availableUpdate?.name;
|
||||||
const currentProgress = availableUpdate ? downloadProgress[availableUpdate.name] || 0 : 0;
|
const currentProgress = availableUpdate ? downloadProgress[availableUpdate.name] || 0 : 0;
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { ActionIcon, Tooltip } from '@mantine/core';
|
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { CircleFadingArrowUp, Download } from 'lucide-react';
|
import { CircleFadingArrowUp, Download } from 'lucide-react';
|
||||||
import { type MouseEvent, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { TITLEBAR_HEIGHT } from '@/constants';
|
import { TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
||||||
|
|
||||||
|
|
@ -17,7 +19,9 @@ export const UpdateButton = () => {
|
||||||
|
|
||||||
const [showDownload, setShowDownload] = useState(false);
|
const [showDownload, setShowDownload] = useState(false);
|
||||||
|
|
||||||
if (!hasUpdate) return null;
|
if (!hasUpdate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let color: 'green' | 'blue' | 'orange' = 'orange';
|
let color: 'green' | 'blue' | 'orange' = 'orange';
|
||||||
let label = 'Update available';
|
let label = 'Update available';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AppShell, Center, Loader, Stack, Text, useMantineColorScheme } from '@mantine/core';
|
import { AppShell, Center, Loader, Stack, Text, useMantineColorScheme } from '@mantine/core';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { BackendCrashModal } from '@/components/App/BackendCrashModal';
|
import { BackendCrashModal } from '@/components/App/BackendCrashModal';
|
||||||
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
|
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
|
||||||
import { ErrorBoundary } from '@/components/App/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/App/ErrorBoundary';
|
||||||
|
|
@ -41,10 +42,10 @@ export const App = () => {
|
||||||
getDefaultInterfaceTab({
|
getDefaultInterfaceTab({
|
||||||
frontendPreference,
|
frontendPreference,
|
||||||
imageGenerationFrontendPreference,
|
imageGenerationFrontendPreference,
|
||||||
isTextMode,
|
|
||||||
isImageGenerationMode,
|
isImageGenerationMode,
|
||||||
|
isTextMode,
|
||||||
}),
|
}),
|
||||||
[frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode]
|
[frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -66,13 +67,13 @@ export const App = () => {
|
||||||
const updateTray = async () => {
|
const updateTray = async () => {
|
||||||
const config = await window.electronAPI.kobold.getSelectedConfig();
|
const config = await window.electronAPI.kobold.getSelectedConfig();
|
||||||
const displayModel = model || sdmodel || null;
|
const displayModel = model || sdmodel || null;
|
||||||
const modelName = displayModel ? displayModel.split('/').pop() || displayModel : null;
|
const modelName = displayModel ? (displayModel.split('/').pop() ?? displayModel) : null;
|
||||||
|
|
||||||
void window.electronAPI.app.updateTrayState({
|
void window.electronAPI.app.updateTrayState({
|
||||||
screen: currentScreen,
|
config: config ?? null,
|
||||||
model: modelName,
|
model: modelName,
|
||||||
config: config || null,
|
|
||||||
monitoringEnabled: systemMonitoringEnabled,
|
monitoringEnabled: systemMonitoringEnabled,
|
||||||
|
screen: currentScreen,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -141,11 +142,15 @@ export const App = () => {
|
||||||
}, [determineScreen]);
|
}, [determineScreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingRemote || !hasInitialized) return;
|
if (loadingRemote || !hasInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const runUpdateCheck = async () => {
|
const runUpdateCheck = async () => {
|
||||||
const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
|
const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
|
||||||
if (!currentBackend) return;
|
if (!currentBackend) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void checkForUpdates();
|
void checkForUpdates();
|
||||||
};
|
};
|
||||||
|
|
@ -158,7 +163,7 @@ export const App = () => {
|
||||||
() => {
|
() => {
|
||||||
void runUpdateCheck();
|
void runUpdateCheck();
|
||||||
},
|
},
|
||||||
6 * 60 * 60 * 1000
|
6 * 60 * 60 * 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -171,10 +176,10 @@ export const App = () => {
|
||||||
const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
|
const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
|
||||||
|
|
||||||
await handleDownload({
|
await handleDownload({
|
||||||
item: download,
|
|
||||||
isUpdate: true,
|
isUpdate: true,
|
||||||
wasCurrentBinary: true,
|
item: download,
|
||||||
oldBackendPath: currentBackend?.path,
|
oldBackendPath: currentBackend?.path,
|
||||||
|
wasCurrentBinary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|
@ -217,7 +222,7 @@ export const App = () => {
|
||||||
padding={isInterfaceScreen ? 0 : 'md'}
|
padding={isInterfaceScreen ? 0 : 'md'}
|
||||||
>
|
>
|
||||||
<TitleBar
|
<TitleBar
|
||||||
currentScreen={currentScreen || 'launch'}
|
currentScreen={currentScreen ?? 'launch'}
|
||||||
currentTab={activeInterfaceTab}
|
currentTab={activeInterfaceTab}
|
||||||
onEject={() => void handleEject()}
|
onEject={() => void handleEject()}
|
||||||
onTabChange={setActiveInterfaceTab}
|
onTabChange={setActiveInterfaceTab}
|
||||||
|
|
@ -228,12 +233,12 @@ export const App = () => {
|
||||||
style={{
|
style={{
|
||||||
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
||||||
minHeight: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
minHeight: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
||||||
position: 'relative',
|
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
paddingLeft: isInterfaceScreen ? 0 : '.5rem',
|
paddingLeft: isInterfaceScreen ? 0 : '.5rem',
|
||||||
paddingRight: isInterfaceScreen ? 0 : '.5rem',
|
paddingRight: isInterfaceScreen ? 0 : '.5rem',
|
||||||
|
paddingTop: 0,
|
||||||
|
position: 'relative',
|
||||||
top: TITLEBAR_HEIGHT,
|
top: TITLEBAR_HEIGHT,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -254,17 +259,17 @@ export const App = () => {
|
||||||
activeInterfaceTab={activeInterfaceTab}
|
activeInterfaceTab={activeInterfaceTab}
|
||||||
isServerReady={isServerReady}
|
isServerReady={isServerReady}
|
||||||
onWelcomeComplete={() => void handleWelcomeComplete()}
|
onWelcomeComplete={() => void handleWelcomeComplete()}
|
||||||
onDownloadComplete={() => void handleDownloadComplete()}
|
onDownloadComplete={() => handleDownloadComplete()}
|
||||||
onLaunch={handleLaunch}
|
onLaunch={handleLaunch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
<UpdateAvailableModal
|
<UpdateAvailableModal
|
||||||
opened={showUpdateModal && !!binaryUpdateInfo}
|
opened={showUpdateModal && Boolean(binaryUpdateInfo)}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onSkip={skipUpdate}
|
onSkip={skipUpdate}
|
||||||
updateInfo={binaryUpdateInfo || undefined}
|
updateInfo={binaryUpdateInfo ?? undefined}
|
||||||
onUpdate={handleBinaryUpdate}
|
onUpdate={handleBinaryUpdate}
|
||||||
/>
|
/>
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Checkbox, Group } from '@mantine/core';
|
import { Checkbox, Group } from '@mantine/core';
|
||||||
|
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
|
|
||||||
interface CheckboxWithTooltipProps {
|
interface CheckboxWithTooltipProps {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Badge, Button, Card, Group, Loader, Progress, rem, Stack, Text } from '@mantine/core';
|
import { Badge, Button, Card, Group, Loader, Progress, Stack, Text, rem } from '@mantine/core';
|
||||||
import { Download, Trash2 } from 'lucide-react';
|
import { Download, Trash2 } from 'lucide-react';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import type { BackendInfo } from '@/types';
|
import type { BackendInfo } from '@/types';
|
||||||
|
|
@ -35,7 +36,7 @@ export const DownloadCard = ({
|
||||||
const isLoading = downloading === backend.name;
|
const isLoading = downloading === backend.name;
|
||||||
const currentProgress = isLoading ? Math.min(downloadProgress[backend.name], 100) || 0 : 0;
|
const currentProgress = isLoading ? Math.min(downloadProgress[backend.name], 100) || 0 : 0;
|
||||||
const hasVersionMismatch = Boolean(
|
const hasVersionMismatch = Boolean(
|
||||||
backend.version && backend.actualVersion && backend.version !== backend.actualVersion
|
backend.version && backend.actualVersion && backend.version !== backend.actualVersion,
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderActionButtons = () => {
|
const renderActionButtons = () => {
|
||||||
|
|
@ -54,7 +55,7 @@ export const DownloadCard = ({
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
<Download style={{ height: rem(14), width: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -73,7 +74,7 @@ export const DownloadCard = ({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
Make Current
|
Make Current
|
||||||
</Button>
|
</Button>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,12 +92,12 @@ export const DownloadCard = ({
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
<Download style={{ height: rem(14), width: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Updating...' : `Update to ${backend.newerVersion}`}
|
{isLoading ? 'Updating...' : `Update to ${backend.newerVersion}`}
|
||||||
</Button>
|
</Button>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,12 +115,12 @@ export const DownloadCard = ({
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
<Download style={{ height: rem(14), width: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Re-downloading...' : 'Re-download'}
|
{isLoading ? 'Re-downloading...' : 'Re-download'}
|
||||||
</Button>
|
</Button>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,12 +138,12 @@ export const DownloadCard = ({
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Trash2 style={{ width: rem(14), height: rem(14) }} />
|
<Trash2 style={{ height: rem(14), width: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Deleting...' : 'Delete'}
|
{isLoading ? 'Deleting...' : 'Delete'}
|
||||||
</Button>
|
</Button>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,8 +156,8 @@ export const DownloadCard = ({
|
||||||
radius="sm"
|
radius="sm"
|
||||||
padding="sm"
|
padding="sm"
|
||||||
{...(backend.isCurrent && {
|
{...(backend.isCurrent && {
|
||||||
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
|
||||||
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
||||||
|
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ActionIcon, Card, Group, rem, Stack, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Card, Group, Stack, Text, Tooltip, rem } from '@mantine/core';
|
||||||
import { Copy } from 'lucide-react';
|
import { Copy } from 'lucide-react';
|
||||||
|
|
||||||
import { safeExecute } from '@/utils/logger';
|
import { safeExecute } from '@/utils/logger';
|
||||||
|
|
||||||
export interface InfoItem {
|
export interface InfoItem {
|
||||||
|
|
@ -19,7 +20,7 @@ export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
|
||||||
|
|
||||||
await safeExecute(
|
await safeExecute(
|
||||||
() => navigator.clipboard.writeText(info),
|
() => navigator.clipboard.writeText(info),
|
||||||
`Failed to copy ${title.toLowerCase()}`
|
`Failed to copy ${title.toLowerCase()}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,12 +47,12 @@ export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
|
||||||
aria-label={`Copy ${title}`}
|
aria-label={`Copy ${title}`}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
|
||||||
right: 8,
|
right: 8,
|
||||||
|
top: 8,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Copy style={{ width: rem(14), height: rem(14) }} />
|
<Copy style={{ height: rem(14), width: rem(14) }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
@ -69,8 +70,8 @@ export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
|
||||||
size="sm"
|
size="sm"
|
||||||
ff="monospace"
|
ff="monospace"
|
||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-all',
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
wordBreak: 'break-all',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.value}
|
{item.value}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Group, Text } from '@mantine/core';
|
import { Group, Text } from '@mantine/core';
|
||||||
|
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
|
|
||||||
interface LabelWithTooltipProps {
|
interface LabelWithTooltipProps {
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import type { ReactNode } from 'react';
|
||||||
const TITLEBAR_HEIGHT = '2.5rem';
|
const TITLEBAR_HEIGHT = '2.5rem';
|
||||||
|
|
||||||
const MODAL_STYLES_WITH_TITLEBAR = {
|
const MODAL_STYLES_WITH_TITLEBAR = {
|
||||||
overlay: {
|
|
||||||
top: TITLEBAR_HEIGHT,
|
|
||||||
},
|
|
||||||
content: {
|
content: {
|
||||||
marginTop: TITLEBAR_HEIGHT,
|
marginTop: TITLEBAR_HEIGHT,
|
||||||
},
|
},
|
||||||
|
overlay: {
|
||||||
|
top: TITLEBAR_HEIGHT,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
|
|
@ -39,10 +39,10 @@ export const Modal = ({
|
||||||
const content = tallContent ? (
|
const content = tallContent ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '75vh',
|
|
||||||
padding: 0,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
height: '75vh',
|
||||||
|
padding: 0,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -54,10 +54,10 @@ export const Modal = ({
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
padding: '1rem 0',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: '1rem 0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button onClick={onClose} variant="filled">
|
<Button onClick={onClose} variant="filled">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button, Group, Text } from '@mantine/core';
|
import { Button, Group, Text } from '@mantine/core';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface CloseConfirmModalProps {
|
interface CloseConfirmModalProps {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { ActionIcon, Box, Paper } from '@mantine/core';
|
import { ActionIcon, Box, Paper } from '@mantine/core';
|
||||||
import { Minus } from 'lucide-react';
|
import { Minus } from 'lucide-react';
|
||||||
import { type MouseEvent, useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad';
|
import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
||||||
import { NotepadEditor } from './Editor.tsx';
|
import { NotepadEditor } from './Editor.tsx';
|
||||||
import { NotepadTabs } from './Tabs.tsx';
|
import { NotepadTabs } from './Tabs.tsx';
|
||||||
|
|
@ -41,7 +44,9 @@ export const NotepadContainer = () => {
|
||||||
|
|
||||||
const handleTabCloseRequest = (title: string) => {
|
const handleTabCloseRequest = (title: string) => {
|
||||||
const tab = tabs.find((t) => t.title === title);
|
const tab = tabs.find((t) => t.title === title);
|
||||||
if (!tab) return;
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tab.content.trim().length > 0) {
|
if (tab.content.trim().length > 0) {
|
||||||
setConfirmCloseModal({
|
setConfirmCloseModal({
|
||||||
|
|
@ -71,7 +76,9 @@ export const NotepadContainer = () => {
|
||||||
const handleMouseMove = (e: globalThis.MouseEvent) => {
|
const handleMouseMove = (e: globalThis.MouseEvent) => {
|
||||||
if (resizeDirection) {
|
if (resizeDirection) {
|
||||||
const rect = containerRef.current?.getBoundingClientRect();
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
if (!rect) return;
|
if (!rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let newWidth = position.width;
|
let newWidth = position.width;
|
||||||
let newHeight = position.height;
|
let newHeight = position.height;
|
||||||
|
|
@ -85,8 +92,8 @@ export const NotepadContainer = () => {
|
||||||
|
|
||||||
setPosition({
|
setPosition({
|
||||||
...position,
|
...position,
|
||||||
width: newWidth,
|
|
||||||
height: newHeight,
|
height: newHeight,
|
||||||
|
width: newWidth,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -106,7 +113,9 @@ export const NotepadContainer = () => {
|
||||||
}
|
}
|
||||||
}, [resizeDirection, position, setPosition]);
|
}, [resizeDirection, position, setPosition]);
|
||||||
|
|
||||||
if (!isLoaded || !isVisible) return null;
|
if (!isLoaded || !isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
|
|
@ -114,52 +123,52 @@ export const NotepadContainer = () => {
|
||||||
shadow="lg"
|
shadow="lg"
|
||||||
withBorder
|
withBorder
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
|
||||||
left: 0,
|
|
||||||
bottom: 24,
|
|
||||||
width: position.width,
|
|
||||||
height: position.height,
|
|
||||||
zIndex: 100,
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
resolvedColorScheme === 'dark'
|
resolvedColorScheme === 'dark'
|
||||||
? 'var(--mantine-color-dark-6)'
|
? 'var(--mantine-color-dark-6)'
|
||||||
: 'var(--mantine-color-white)',
|
: 'var(--mantine-color-white)',
|
||||||
|
bottom: 24,
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
|
height: position.height,
|
||||||
|
left: 0,
|
||||||
|
position: 'fixed',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
width: position.width,
|
||||||
|
zIndex: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
cursor: 'ns-resize',
|
||||||
|
height: 4,
|
||||||
|
left: 0,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -2,
|
top: -2,
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 4,
|
|
||||||
cursor: 'ns-resize',
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleResizeStart('top')}
|
onMouseDown={handleResizeStart('top')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
right: -2,
|
|
||||||
width: 4,
|
|
||||||
height: '100%',
|
|
||||||
cursor: 'ew-resize',
|
cursor: 'ew-resize',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
right: -2,
|
||||||
|
top: 0,
|
||||||
|
width: 4,
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleResizeStart('right')}
|
onMouseDown={handleResizeStart('right')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
top: -2,
|
|
||||||
right: -2,
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
cursor: 'ne-resize',
|
cursor: 'ne-resize',
|
||||||
|
height: 12,
|
||||||
|
position: 'absolute',
|
||||||
|
right: -2,
|
||||||
|
top: -2,
|
||||||
|
width: 12,
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleResizeStart('top-right')}
|
onMouseDown={handleResizeStart('top-right')}
|
||||||
/>
|
/>
|
||||||
|
|
@ -167,12 +176,6 @@ export const NotepadContainer = () => {
|
||||||
{resizeDirection && (
|
{resizeDirection && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh',
|
|
||||||
zIndex: 9999,
|
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
cursor:
|
cursor:
|
||||||
resizeDirection.includes('right') && resizeDirection.includes('top')
|
resizeDirection.includes('right') && resizeDirection.includes('top')
|
||||||
|
|
@ -180,6 +183,12 @@ export const NotepadContainer = () => {
|
||||||
: resizeDirection.includes('right')
|
: resizeDirection.includes('right')
|
||||||
? 'ew-resize'
|
? 'ew-resize'
|
||||||
: 'ns-resize',
|
: 'ns-resize',
|
||||||
|
height: '100vh',
|
||||||
|
left: 0,
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
width: '100vw',
|
||||||
|
zIndex: 9999,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -187,14 +196,14 @@ export const NotepadContainer = () => {
|
||||||
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderBottom: `1px solid ${
|
borderBottom: `1px solid ${
|
||||||
resolvedColorScheme === 'dark'
|
resolvedColorScheme === 'dark'
|
||||||
? 'var(--mantine-color-dark-4)'
|
? 'var(--mantine-color-dark-4)'
|
||||||
: 'var(--mantine-color-gray-3)'
|
: 'var(--mantine-color-gray-3)'
|
||||||
}`,
|
}`,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
minHeight: 28,
|
minHeight: 28,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -203,10 +212,10 @@ export const NotepadContainer = () => {
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexShrink: 0,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
paddingLeft: 8,
|
paddingLeft: 8,
|
||||||
paddingRight: 8,
|
paddingRight: 8,
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>
|
<ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ import { search } from '@codemirror/search';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
|
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
import { type MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import type { NotepadTab } from '@/types/electron';
|
import type { NotepadTab } from '@/types/electron';
|
||||||
|
|
@ -34,7 +37,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
|
|
||||||
setSaveTimeout(timeout);
|
setSaveTimeout(timeout);
|
||||||
},
|
},
|
||||||
[tab.title, saveTabContent, saveTimeout]
|
[tab.title, saveTabContent, saveTimeout],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEditorContextMenu = (e: MouseEvent) => {
|
const handleEditorContextMenu = (e: MouseEvent) => {
|
||||||
|
|
@ -57,7 +60,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
clearTimeout(saveTimeout);
|
clearTimeout(saveTimeout);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[saveTimeout]
|
[saveTimeout],
|
||||||
);
|
);
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
|
@ -81,9 +84,9 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
onClick={handleBoxClick}
|
onClick={handleBoxClick}
|
||||||
onContextMenu={handleEditorContextMenu}
|
onContextMenu={handleEditorContextMenu}
|
||||||
style={{
|
style={{
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
|
@ -93,20 +96,20 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
theme={theme}
|
theme={theme}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: showLineNumbers,
|
|
||||||
foldGutter: false,
|
|
||||||
dropCursor: false,
|
|
||||||
allowMultipleSelections: false,
|
allowMultipleSelections: false,
|
||||||
indentOnInput: true,
|
autocompletion: true,
|
||||||
bracketMatching: true,
|
bracketMatching: true,
|
||||||
closeBrackets: true,
|
closeBrackets: true,
|
||||||
autocompletion: true,
|
dropCursor: false,
|
||||||
|
foldGutter: false,
|
||||||
highlightSelectionMatches: false,
|
highlightSelectionMatches: false,
|
||||||
|
indentOnInput: true,
|
||||||
|
lineNumbers: showLineNumbers,
|
||||||
searchKeymap: true,
|
searchKeymap: true,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
|
||||||
flex: '1 1 0',
|
flex: '1 1 0',
|
||||||
|
height: '100%',
|
||||||
minHeight: '0',
|
minHeight: '0',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { ActionIcon, Box, Text, TextInput } from '@mantine/core';
|
import { ActionIcon, Box, Text, TextInput } from '@mantine/core';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import {
|
import { useEffect, useRef, useState } from 'react';
|
||||||
type DragEvent,
|
import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||||
type KeyboardEvent,
|
|
||||||
type MouseEvent,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
|
|
@ -109,7 +104,7 @@ export const Tab = ({
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={(e) => onDrop(e, index)}
|
onDrop={(e) => onDrop(e, index)}
|
||||||
style={{
|
style={{
|
||||||
padding: '0.375rem 0.5rem',
|
alignItems: 'center',
|
||||||
backgroundColor: isActive
|
backgroundColor: isActive
|
||||||
? resolvedColorScheme === 'dark'
|
? resolvedColorScheme === 'dark'
|
||||||
? 'var(--mantine-color-dark-4)'
|
? 'var(--mantine-color-dark-4)'
|
||||||
|
|
@ -126,11 +121,11 @@ export const Tab = ({
|
||||||
}`,
|
}`,
|
||||||
cursor: isEditing ? 'default' : 'pointer',
|
cursor: isEditing ? 'default' : 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.25rem',
|
gap: '0.25rem',
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: '7.5rem',
|
maxWidth: '7.5rem',
|
||||||
|
minWidth: 0,
|
||||||
opacity: isDragOver ? 0.5 : 1,
|
opacity: isDragOver ? 0.5 : 1,
|
||||||
|
padding: '0.375rem 0.5rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
|
|
@ -149,10 +144,10 @@ export const Tab = ({
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
fontSize: 'var(--mantine-font-size-xs)',
|
fontSize: 'var(--mantine-font-size-xs)',
|
||||||
padding: 0,
|
|
||||||
minHeight: 'auto',
|
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
|
minHeight: 'auto',
|
||||||
|
padding: 0,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -168,10 +163,10 @@ export const Tab = ({
|
||||||
setShowLineNumbers(!showLineNumbers);
|
setShowLineNumbers(!showLineNumbers);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
flex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
flex: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { ActionIcon, Box } from '@mantine/core';
|
import { ActionIcon, Box } from '@mantine/core';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { type DragEvent, type MouseEvent, useState } from 'react';
|
import type { DragEvent, MouseEvent } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Tab } from '@/components/Notepad/Tab';
|
import { Tab } from '@/components/Notepad/Tab';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
@ -10,6 +12,11 @@ interface NotepadTabsProps {
|
||||||
onCloseTab: (title: string) => void;
|
onCloseTab: (title: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
};
|
||||||
|
|
||||||
export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) => {
|
export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) => {
|
||||||
const {
|
const {
|
||||||
tabs,
|
tabs,
|
||||||
|
|
@ -51,11 +58,6 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.dataTransfer.dropEffect = 'move';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragEnter = (index: number) => {
|
const handleDragEnter = (index: number) => {
|
||||||
setDragOverIndex(index);
|
setDragOverIndex(index);
|
||||||
};
|
};
|
||||||
|
|
@ -85,8 +87,8 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
: 'var(--mantine-color-gray-3)'
|
: 'var(--mantine-color-gray-3)'
|
||||||
}`,
|
}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
overflow: 'hidden',
|
|
||||||
minHeight: '2rem',
|
minHeight: '2rem',
|
||||||
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
|
|
@ -117,8 +119,8 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => void onCreateNewTab()}
|
onClick={() => void onCreateNewTab()}
|
||||||
style={{
|
style={{
|
||||||
margin: '0.25rem',
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
margin: '0.25rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus size="0.75rem" />
|
<Plus size="0.75rem" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { ComboboxItem } from '@mantine/core';
|
import type { ComboboxItem } from '@mantine/core';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
import type { SelectOption } from '@/types';
|
import type { SelectOption } from '@/types';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { Switch as MantineSwitch, type SwitchProps } from '@mantine/core';
|
import { Switch as MantineSwitch } from '@mantine/core';
|
||||||
|
import type { SwitchProps } from '@mantine/core';
|
||||||
|
|
||||||
export const Switch = (props: SwitchProps) => (
|
export const Switch = (props: SwitchProps) => (
|
||||||
<MantineSwitch
|
<MantineSwitch
|
||||||
{...props}
|
{...props}
|
||||||
styles={{
|
styles={{
|
||||||
track: { transition: 'none' },
|
|
||||||
thumb: { transition: 'none' },
|
thumb: { transition: 'none' },
|
||||||
|
track: { transition: 'none' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
|
import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
|
|
@ -33,8 +34,8 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
setDownloadingAsset(download.name);
|
setDownloadingAsset(download.name);
|
||||||
|
|
||||||
await handleDownloadFromStore({
|
await handleDownloadFromStore({
|
||||||
item: download,
|
|
||||||
isUpdate: false,
|
isUpdate: false,
|
||||||
|
item: download,
|
||||||
wasCurrentBinary: false,
|
wasCurrentBinary: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
setDownloadingAsset(null);
|
setDownloadingAsset(null);
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
[handleDownloadFromStore, onDownloadComplete]
|
[handleDownloadFromStore, onDownloadComplete],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -79,13 +80,13 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
backend={{
|
backend={{
|
||||||
name: download.name,
|
|
||||||
version: download.version || '',
|
|
||||||
size: download.size,
|
|
||||||
isInstalled: false,
|
|
||||||
isCurrent: false,
|
|
||||||
downloadUrl: download.url,
|
downloadUrl: download.url,
|
||||||
hasUpdate: false,
|
hasUpdate: false,
|
||||||
|
isCurrent: false,
|
||||||
|
isInstalled: false,
|
||||||
|
name: download.name,
|
||||||
|
size: download.size,
|
||||||
|
version: download.version ?? '',
|
||||||
}}
|
}}
|
||||||
size={formatDownloadSize(download.size, download.url)}
|
size={formatDownloadSize(download.size, download.url)}
|
||||||
description={getAssetDescription(download.name)}
|
description={getAssetDescription(download.name)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Box, Stack, Text } from '@mantine/core';
|
import { Box, Stack, Text } from '@mantine/core';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
@ -24,18 +25,18 @@ export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
|
||||||
imageGenerationFrontendPreference,
|
imageGenerationFrontendPreference,
|
||||||
isImageGenerationMode: effectiveImageMode,
|
isImageGenerationMode: effectiveImageMode,
|
||||||
}),
|
}),
|
||||||
[frontendPreference, imageGenerationFrontendPreference, effectiveImageMode]
|
[frontendPreference, imageGenerationFrontendPreference, effectiveImageMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isServerReady) {
|
if (!isServerReady) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack align="center" gap="md" mt="xl">
|
<Stack align="center" gap="md" mt="xl">
|
||||||
|
|
@ -54,21 +55,22 @@ export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
|
||||||
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
src={iframeUrl}
|
src={iframeUrl}
|
||||||
title={title}
|
title={title}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: 'inherit',
|
borderRadius: 'inherit',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
allow="clipboard-read; clipboard-write; fullscreen; microphone; geolocation; camera; autoplay"
|
allow="clipboard-read; clipboard-write; fullscreen; microphone; geolocation; camera; autoplay"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ActionIcon, Box, ScrollArea } from '@mantine/core';
|
import { ActionIcon, Box, ScrollArea } from '@mantine/core';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
||||||
|
|
@ -28,7 +29,9 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleScroll = ({ y }: { y: number }) => {
|
const handleScroll = ({ y }: { y: number }) => {
|
||||||
if (!viewportRef.current) return;
|
if (!viewportRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { scrollHeight, clientHeight } = viewportRef.current;
|
const { scrollHeight, clientHeight } = viewportRef.current;
|
||||||
const isAtBottomNow = y + clientHeight >= scrollHeight - 10;
|
const isAtBottomNow = y + clientHeight >= scrollHeight - 10;
|
||||||
|
|
@ -83,14 +86,14 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
colorScheme === 'dark'
|
colorScheme === 'dark'
|
||||||
? 'var(--mantine-color-dark-filled)'
|
? 'var(--mantine-color-dark-filled)'
|
||||||
: 'var(--mantine-color-gray-0)',
|
: 'var(--mantine-color-gray-0)',
|
||||||
borderRadius: 'inherit',
|
borderRadius: 'inherit',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -110,19 +113,19 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
|
||||||
fontFamily:
|
|
||||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
||||||
fontSize: '0.875em',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
color:
|
color:
|
||||||
colorScheme === 'dark'
|
colorScheme === 'dark'
|
||||||
? 'var(--mantine-color-gray-0)'
|
? 'var(--mantine-color-gray-0)'
|
||||||
: 'var(--mantine-color-dark-filled)',
|
: 'var(--mantine-color-dark-filled)',
|
||||||
whiteSpace: 'pre-wrap',
|
fontFamily:
|
||||||
wordBreak: 'break-word',
|
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: '0.875em',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
margin: 0,
|
||||||
opacity: isVisible ? 1 : 0,
|
opacity: isVisible ? 1 : 0,
|
||||||
transition: 'opacity 0.2s ease-in-out',
|
transition: 'opacity 0.2s ease-in-out',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: processTerminalContent(terminalContent),
|
__html: processTerminalContent(terminalContent),
|
||||||
|
|
@ -139,11 +142,11 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={scrollToBottom}
|
onClick={scrollToBottom}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
bottom: '1.25rem',
|
bottom: '1.25rem',
|
||||||
|
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||||
|
position: 'absolute',
|
||||||
right: '1.25rem',
|
right: '1.25rem',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
|
||||||
}}
|
}}
|
||||||
aria-label="Scroll to bottom"
|
aria-label="Scroll to bottom"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
||||||
import { TerminalTab, type TerminalTabRef } from '@/components/screens/Interface/TerminalTab';
|
import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
|
||||||
|
import type { TerminalTabRef } from '@/components/screens/Interface/TerminalTab';
|
||||||
import type { InterfaceTab } from '@/types';
|
import type { InterfaceTab } from '@/types';
|
||||||
|
|
||||||
interface InterfaceScreenProps {
|
interface InterfaceScreenProps {
|
||||||
|
|
@ -20,24 +22,24 @@ export const InterfaceScreen = ({ activeTab, isServerReady }: InterfaceScreenPro
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
display: activeTab === 'chat-text' || activeTab === 'chat-image' ? 'block' : 'none',
|
display: activeTab === 'chat-text' || activeTab === 'chat-image' ? 'block' : 'none',
|
||||||
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerTab isServerReady={isServerReady} activeTab={activeTab || undefined} />
|
<ServerTab isServerReady={isServerReady} activeTab={activeTab ?? undefined} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
display: activeTab === 'terminal' ? 'block' : 'none',
|
display: activeTab === 'terminal' ? 'block' : 'none',
|
||||||
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TerminalTab ref={terminalTabRef} />
|
<TerminalTab ref={terminalTabRef} />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ActionIcon, Button, Group, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
|
import { ActionIcon, Button, Group, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
|
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
|
||||||
|
|
@ -38,11 +39,11 @@ export const AdvancedTab = () => {
|
||||||
|
|
||||||
if (support) {
|
if (support) {
|
||||||
setBackendSupport({
|
setBackendSupport({
|
||||||
noavx2: support.noavx2,
|
|
||||||
failsafe: support.failsafe,
|
failsafe: support.failsafe,
|
||||||
|
noavx2: support.noavx2,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setBackendSupport({ noavx2: false, failsafe: false });
|
setBackendSupport({ failsafe: false, noavx2: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Accordion, Badge, Button, Code, Group, Stack, Text, TextInput } from '@mantine/core';
|
import { Accordion, Badge, Button, Code, Group, Stack, Text, TextInput } from '@mantine/core';
|
||||||
import { Code as CodeIcon } from 'lucide-react';
|
import { Code as CodeIcon } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface CommandLineArgumentsModalProps {
|
interface CommandLineArgumentsModalProps {
|
||||||
|
|
@ -95,684 +96,684 @@ const IGNORED_ARGS = new Set([
|
||||||
|
|
||||||
const COMMAND_LINE_ARGUMENTS = [
|
const COMMAND_LINE_ARGUMENTS = [
|
||||||
{
|
{
|
||||||
flag: '--threads',
|
|
||||||
aliases: ['-t'],
|
aliases: ['-t'],
|
||||||
|
category: 'Basic',
|
||||||
description:
|
description:
|
||||||
'Use a custom number of threads if specified. Otherwise, uses an amount based on CPU cores',
|
'Use a custom number of threads if specified. Otherwise, uses an amount based on CPU cores',
|
||||||
|
flag: '--threads',
|
||||||
metavar: '[threads]',
|
metavar: '[threads]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
category: 'Basic',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--analyze',
|
|
||||||
description: 'Reads the metadata, weight types and tensor names in any GGUF file.',
|
|
||||||
metavar: '[filename]',
|
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
|
description: 'Reads the metadata, weight types and tensor names in any GGUF file.',
|
||||||
|
flag: '--analyze',
|
||||||
|
metavar: '[filename]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--maingpu',
|
|
||||||
aliases: ['--main-gpu', '-mg'],
|
aliases: ['--main-gpu', '-mg'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: -1,
|
||||||
description:
|
description:
|
||||||
'Only used in a multi-gpu setup. Sets the index of the main GPU that will be used.',
|
'Only used in a multi-gpu setup. Sets the index of the main GPU that will be used.',
|
||||||
|
flag: '--maingpu',
|
||||||
metavar: '[Device ID]',
|
metavar: '[Device ID]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: -1,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--batchsize',
|
|
||||||
aliases: ['--blasbatchsize', '--batch-size', '-b'],
|
aliases: ['--blasbatchsize', '--batch-size', '-b'],
|
||||||
description:
|
category: 'Advanced',
|
||||||
'Sets the batch size used in batched processing (default 512). Setting it to -1 disables batched mode, but keeps other benefits like GPU offload.',
|
|
||||||
type: 'int',
|
|
||||||
choices: ['-1', '16', '32', '64', '128', '256', '512', '1024', '2048', '4096'],
|
choices: ['-1', '16', '32', '64', '128', '256', '512', '1024', '2048', '4096'],
|
||||||
default: 512,
|
default: 512,
|
||||||
category: 'Advanced',
|
description:
|
||||||
|
'Sets the batch size used in batched processing (default 512). Setting it to -1 disables batched mode, but keeps other benefits like GPU offload.',
|
||||||
|
flag: '--batchsize',
|
||||||
|
type: 'int',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--blasthreads',
|
|
||||||
aliases: ['--batchthreads', '--threadsbatch', '--threads-batch'],
|
aliases: ['--batchthreads', '--threadsbatch', '--threads-batch'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Use a different number of threads during batching if specified. Otherwise, has the same value as --threads',
|
'Use a different number of threads during batching if specified. Otherwise, has the same value as --threads',
|
||||||
|
flag: '--blasthreads',
|
||||||
metavar: '[threads]',
|
metavar: '[threads]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--lora',
|
category: 'Advanced',
|
||||||
description: 'GGUF models only, applies a lora file on top of model.',
|
description: 'GGUF models only, applies a lora file on top of model.',
|
||||||
|
flag: '--lora',
|
||||||
metavar: '[lora_filename]',
|
metavar: '[lora_filename]',
|
||||||
type: 'string[]',
|
type: 'string[]',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--loramult',
|
category: 'Advanced',
|
||||||
|
default: 1,
|
||||||
description: 'Multiplier for the Text LORA model to be applied.',
|
description: 'Multiplier for the Text LORA model to be applied.',
|
||||||
|
flag: '--loramult',
|
||||||
metavar: '[amount]',
|
metavar: '[amount]',
|
||||||
type: 'float',
|
type: 'float',
|
||||||
default: 1.0,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--nofastforward',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'If set, do not attempt to fast forward GGUF context (always reprocess). Will also enable noshift',
|
'If set, do not attempt to fast forward GGUF context (always reprocess). Will also enable noshift',
|
||||||
|
flag: '--nofastforward',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--useswa',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'If set, allows Sliding Window Attention (SWA) KV Cache, which saves memory but cannot be used with context shifting.',
|
'If set, allows Sliding Window Attention (SWA) KV Cache, which saves memory but cannot be used with context shifting.',
|
||||||
|
flag: '--useswa',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ropeconfig',
|
category: 'Advanced',
|
||||||
|
default: '0.0 10000.0',
|
||||||
description:
|
description:
|
||||||
'If set, uses customized RoPE scaling from configured frequency scale and frequency base (e.g. --ropeconfig 0.25 10000). Otherwise, uses NTK-Aware scaling set automatically based on context size. For linear rope, simply set the freq-scale and ignore the freq-base',
|
'If set, uses customized RoPE scaling from configured frequency scale and frequency base (e.g. --ropeconfig 0.25 10000). Otherwise, uses NTK-Aware scaling set automatically based on context size. For linear rope, simply set the freq-scale and ignore the freq-base',
|
||||||
|
flag: '--ropeconfig',
|
||||||
metavar: '[rope-freq-scale] [rope-freq-base]',
|
metavar: '[rope-freq-scale] [rope-freq-base]',
|
||||||
default: '0.0 10000.0',
|
|
||||||
type: 'float[]',
|
type: 'float[]',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--overridenativecontext',
|
category: 'Advanced',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Overrides the native trained context of the loaded model with a custom value to be used for Rope scaling.',
|
'Overrides the native trained context of the loaded model with a custom value to be used for Rope scaling.',
|
||||||
|
flag: '--overridenativecontext',
|
||||||
metavar: '[trained context]',
|
metavar: '[trained context]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--usemlock',
|
|
||||||
aliases: ['--mlock'],
|
aliases: ['--mlock'],
|
||||||
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Enables mlock, preventing the RAM used to load the model from being paged out. Not usually recommended.',
|
'Enables mlock, preventing the RAM used to load the model from being paged out. Not usually recommended.',
|
||||||
|
flag: '--usemlock',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--onready',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description: 'An optional shell command to execute after the model has been loaded.',
|
description: 'An optional shell command to execute after the model has been loaded.',
|
||||||
|
flag: '--onready',
|
||||||
metavar: '[shell command]',
|
metavar: '[shell command]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--benchmark',
|
category: 'Advanced',
|
||||||
|
default: 'stdout',
|
||||||
description:
|
description:
|
||||||
'Do not start server, instead run benchmarks. If filename is provided, appends results to provided file.',
|
'Do not start server, instead run benchmarks. If filename is provided, appends results to provided file.',
|
||||||
|
flag: '--benchmark',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'stdout',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--prompt',
|
|
||||||
aliases: ['-p'],
|
aliases: ['-p'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Passing a prompt string triggers a direct inference, loading the model, outputs the response to stdout and exits. Can be used alone or with benchmark.',
|
'Passing a prompt string triggers a direct inference, loading the model, outputs the response to stdout and exits. Can be used alone or with benchmark.',
|
||||||
|
flag: '--prompt',
|
||||||
metavar: '[prompt]',
|
metavar: '[prompt]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--cli',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Does not launch KoboldCpp HTTP server. Instead, enables KoboldCpp from the command line, accepting interactive console input and displaying responses to the terminal.',
|
'Does not launch KoboldCpp HTTP server. Instead, enables KoboldCpp from the command line, accepting interactive console input and displaying responses to the terminal.',
|
||||||
|
flag: '--cli',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--genlimit',
|
|
||||||
aliases: ['--promptlimit'],
|
aliases: ['--promptlimit'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Sets the maximum number of generated tokens, it will restrict all generations to this or lower. Also usable with --prompt or --benchmark.',
|
'Sets the maximum number of generated tokens, it will restrict all generations to this or lower. Also usable with --prompt or --benchmark.',
|
||||||
|
flag: '--genlimit',
|
||||||
metavar: '[token limit]',
|
metavar: '[token limit]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--highpriority',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Experimental flag. If set, increases the process CPU priority, potentially speeding up generation. Use caution.',
|
'Experimental flag. If set, increases the process CPU priority, potentially speeding up generation. Use caution.',
|
||||||
|
flag: '--highpriority',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--foreground',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Windows only. Sends the terminal to the foreground every time a new prompt is generated. This helps avoid some idle slowdown issues.',
|
'Windows only. Sends the terminal to the foreground every time a new prompt is generated. This helps avoid some idle slowdown issues.',
|
||||||
|
flag: '--foreground',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--preloadstory',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Configures a prepared story json save file to be hosted on the server, which frontends (such as KoboldAI Lite) can access over the API.',
|
'Configures a prepared story json save file to be hosted on the server, which frontends (such as KoboldAI Lite) can access over the API.',
|
||||||
|
flag: '--preloadstory',
|
||||||
metavar: '[savefile]',
|
metavar: '[savefile]',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--savedatafile',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'If enabled, creates or opens a persistent database file on the server, that allows users to save and load their data remotely. A new file is created if it does not exist.',
|
'If enabled, creates or opens a persistent database file on the server, that allows users to save and load their data remotely. A new file is created if it does not exist.',
|
||||||
|
flag: '--savedatafile',
|
||||||
metavar: '[savefile]',
|
metavar: '[savefile]',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--quiet',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Enable quiet mode, which hides generation inputs and outputs in the terminal. Quiet mode is automatically enabled when running a horde worker.',
|
'Enable quiet mode, which hides generation inputs and outputs in the terminal. Quiet mode is automatically enabled when running a horde worker.',
|
||||||
|
flag: '--quiet',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ssl',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Allows all content to be served over SSL instead. A valid UNENCRYPTED SSL cert and key .pem files must be provided',
|
'Allows all content to be served over SSL instead. A valid UNENCRYPTED SSL cert and key .pem files must be provided',
|
||||||
|
flag: '--ssl',
|
||||||
metavar: '[cert_pem] [key_pem]',
|
metavar: '[cert_pem] [key_pem]',
|
||||||
type: 'string[]',
|
type: 'string[]',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--password',
|
category: 'Advanced',
|
||||||
|
default: 'None',
|
||||||
description:
|
description:
|
||||||
'Enter a password required to use this instance. This key will be required for all text endpoints. Image endpoints are not secured.',
|
'Enter a password required to use this instance. This key will be required for all text endpoints. Image endpoints are not secured.',
|
||||||
|
flag: '--password',
|
||||||
metavar: '[API key]',
|
metavar: '[API key]',
|
||||||
default: 'None',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--mmproj',
|
category: 'Multimodal',
|
||||||
description: 'Select a multimodal projector file for vision models like LLaVA.',
|
|
||||||
metavar: '[filename]',
|
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Multimodal',
|
description: 'Select a multimodal projector file for vision models like LLaVA.',
|
||||||
|
flag: '--mmproj',
|
||||||
|
metavar: '[filename]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--mmprojcpu',
|
|
||||||
aliases: ['--no-mmproj-offload'],
|
aliases: ['--no-mmproj-offload'],
|
||||||
description: 'Force CLIP for Vision mmproj always on CPU.',
|
|
||||||
type: 'boolean',
|
|
||||||
category: 'Multimodal',
|
category: 'Multimodal',
|
||||||
|
description: 'Force CLIP for Vision mmproj always on CPU.',
|
||||||
|
flag: '--mmprojcpu',
|
||||||
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--visionmaxres',
|
category: 'Multimodal',
|
||||||
|
default: 1024,
|
||||||
description:
|
description:
|
||||||
'Clamp MMProj vision maximum allowed resolution. Allowed values are between 512 to 2048 px (default 1024).',
|
'Clamp MMProj vision maximum allowed resolution. Allowed values are between 512 to 2048 px (default 1024).',
|
||||||
|
flag: '--visionmaxres',
|
||||||
metavar: '[max px]',
|
metavar: '[max px]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 1024,
|
|
||||||
category: 'Multimodal',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--draftmodel',
|
|
||||||
aliases: ['--model-draft', '-md'],
|
aliases: ['--model-draft', '-md'],
|
||||||
|
category: 'Speculative Decoding',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Load a small draft model for speculative decoding. It will be fully offloaded. Vocab must match the main model.',
|
'Load a small draft model for speculative decoding. It will be fully offloaded. Vocab must match the main model.',
|
||||||
|
flag: '--draftmodel',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Speculative Decoding',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--draftamount',
|
|
||||||
aliases: ['--draft-max', '--draft-n'],
|
aliases: ['--draft-max', '--draft-n'],
|
||||||
|
category: 'Speculative Decoding',
|
||||||
|
default: 8,
|
||||||
description: 'How many tokens to draft per chunk before verifying results',
|
description: 'How many tokens to draft per chunk before verifying results',
|
||||||
|
flag: '--draftamount',
|
||||||
metavar: '[tokens]',
|
metavar: '[tokens]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 8,
|
|
||||||
category: 'Speculative Decoding',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--draftgpulayers',
|
|
||||||
aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'],
|
aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'],
|
||||||
|
category: 'Speculative Decoding',
|
||||||
|
default: 999,
|
||||||
description: 'How many layers to offload to GPU for the draft model (default=full offload)',
|
description: 'How many layers to offload to GPU for the draft model (default=full offload)',
|
||||||
|
flag: '--draftgpulayers',
|
||||||
metavar: '[layers]',
|
metavar: '[layers]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 999,
|
|
||||||
category: 'Speculative Decoding',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--draftgpusplit',
|
category: 'Speculative Decoding',
|
||||||
description:
|
description:
|
||||||
'GPU layer distribution ratio for draft model (default=same as main). Only works if multi-GPUs selected for MAIN model and tensor_split is set!',
|
'GPU layer distribution ratio for draft model (default=same as main). Only works if multi-GPUs selected for MAIN model and tensor_split is set!',
|
||||||
|
flag: '--draftgpusplit',
|
||||||
metavar: '[Ratios]',
|
metavar: '[Ratios]',
|
||||||
type: 'float[]',
|
type: 'float[]',
|
||||||
category: 'Speculative Decoding',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--defaultgenamt',
|
category: 'Performance',
|
||||||
|
default: 896,
|
||||||
description:
|
description:
|
||||||
'How many tokens to generate by default, if not specified. Must be smaller than context size. Usually, your frontend GUI will override this.',
|
'How many tokens to generate by default, if not specified. Must be smaller than context size. Usually, your frontend GUI will override this.',
|
||||||
|
flag: '--defaultgenamt',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 896,
|
|
||||||
category: 'Performance',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--nobostoken',
|
category: 'Performance',
|
||||||
description:
|
description:
|
||||||
'Prevents BOS token from being added at the start of any prompt. Usually NOT recommended for most models.',
|
'Prevents BOS token from being added at the start of any prompt. Usually NOT recommended for most models.',
|
||||||
|
flag: '--nobostoken',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Performance',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--enableguidance',
|
category: 'Performance',
|
||||||
description:
|
description:
|
||||||
'Enables the use of Classifier-Free-Guidance, which allows the use of negative prompts. Has performance and memory impact.',
|
'Enables the use of Classifier-Free-Guidance, which allows the use of negative prompts. Has performance and memory impact.',
|
||||||
|
flag: '--enableguidance',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Performance',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ratelimit',
|
category: 'Advanced',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'If enabled, rate limit generative request by IP address. Each IP can only send a new request once per X seconds.',
|
'If enabled, rate limit generative request by IP address. Each IP can only send a new request once per X seconds.',
|
||||||
|
flag: '--ratelimit',
|
||||||
metavar: '[seconds]',
|
metavar: '[seconds]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ignoremissing',
|
category: 'Advanced',
|
||||||
description: 'Ignores all missing non-essential files, just skipping them instead.',
|
description: 'Ignores all missing non-essential files, just skipping them instead.',
|
||||||
|
flag: '--ignoremissing',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--chatcompletionsadapter',
|
category: 'Advanced',
|
||||||
|
default: 'AutoGuess',
|
||||||
description:
|
description:
|
||||||
'Select an optional ChatCompletions Adapter JSON file to force custom instruct tags.',
|
'Select an optional ChatCompletions Adapter JSON file to force custom instruct tags.',
|
||||||
|
flag: '--chatcompletionsadapter',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: 'AutoGuess',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--jinja',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done without jinja.',
|
'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done without jinja.',
|
||||||
|
flag: '--jinja',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--jinja_tools',
|
|
||||||
aliases: ['--jinja-tools', '--jinjatools'],
|
aliases: ['--jinja-tools', '--jinjatools'],
|
||||||
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done with jinja.',
|
'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done with jinja.',
|
||||||
|
flag: '--jinja_tools',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--smartcontext',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Reserving a portion of context to try processing less frequently. Outdated. Not recommended.',
|
'Reserving a portion of context to try processing less frequently. Outdated. Not recommended.',
|
||||||
|
flag: '--smartcontext',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--unpack',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description: 'Extracts the file contents of the KoboldCpp binary into a target directory.',
|
description: 'Extracts the file contents of the KoboldCpp binary into a target directory.',
|
||||||
|
flag: '--unpack',
|
||||||
metavar: 'destination',
|
metavar: 'destination',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--exportconfig',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description: 'Exports the current selected arguments as a .kcpps settings file',
|
description: 'Exports the current selected arguments as a .kcpps settings file',
|
||||||
|
flag: '--exportconfig',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--exporttemplate',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description: 'Exports the current selected arguments as a .kcppt template file',
|
description: 'Exports the current selected arguments as a .kcppt template file',
|
||||||
|
flag: '--exporttemplate',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--nomodel',
|
category: 'Advanced',
|
||||||
description: 'Allows you to launch the GUI alone, without selecting any model.',
|
description: 'Allows you to launch the GUI alone, without selecting any model.',
|
||||||
|
flag: '--nomodel',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--singleinstance',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Allows this KoboldCpp instance to be shut down by any new instance requesting the same port, preventing duplicate servers from clashing on a port.',
|
'Allows this KoboldCpp instance to be shut down by any new instance requesting the same port, preventing duplicate servers from clashing on a port.',
|
||||||
|
flag: '--singleinstance',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--gendefaults',
|
category: 'Advanced',
|
||||||
description: 'Sets extra default parameters for some fields in API requests, as a JSON string.',
|
|
||||||
metavar: '{"parameter":"value",...}',
|
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Advanced',
|
description: 'Sets extra default parameters for some fields in API requests, as a JSON string.',
|
||||||
|
flag: '--gendefaults',
|
||||||
|
metavar: '{"parameter":"value",...}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--gendefaultsoverwrite',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
'Allow the gendefaults parameters to overwrite the original value in API payloads.',
|
'Allow the gendefaults parameters to overwrite the original value in API payloads.',
|
||||||
|
flag: '--gendefaultsoverwrite',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--maxrequestsize',
|
category: 'Advanced',
|
||||||
|
default: 32,
|
||||||
description:
|
description:
|
||||||
'Specify a max request payload size. Any requests to the server larger than this size will be dropped. Do not change if unsure.',
|
'Specify a max request payload size. Any requests to the server larger than this size will be dropped. Do not change if unsure.',
|
||||||
|
flag: '--maxrequestsize',
|
||||||
metavar: '[size in MB]',
|
metavar: '[size in MB]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 32,
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--overridekv',
|
|
||||||
aliases: ['--override-kv'],
|
aliases: ['--override-kv'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Override metadata value by key. Separate multiple values with commas. Format is name=type:value. Types: int, float, bool, str',
|
'Override metadata value by key. Separate multiple values with commas. Format is name=type:value. Types: int, float, bool, str',
|
||||||
|
flag: '--overridekv',
|
||||||
metavar: '[name=type:value]',
|
metavar: '[name=type:value]',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--overridetensors',
|
|
||||||
aliases: ['--override-tensor', '-ot'],
|
aliases: ['--override-tensor', '-ot'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Override selected backend for specific tensors matching tensor_name_regex_pattern=buffer_type, same as in llama.cpp.',
|
'Override selected backend for specific tensors matching tensor_name_regex_pattern=buffer_type, same as in llama.cpp.',
|
||||||
|
flag: '--overridetensors',
|
||||||
metavar: '[tensor name pattern=buffer type]',
|
metavar: '[tensor name pattern=buffer type]',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--admin',
|
category: 'Administration',
|
||||||
description:
|
description:
|
||||||
'Enables admin mode, allowing you to unload and reload different configurations or models.',
|
'Enables admin mode, allowing you to unload and reload different configurations or models.',
|
||||||
|
flag: '--admin',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Administration',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--adminpassword',
|
category: 'Administration',
|
||||||
|
default: 'None',
|
||||||
description:
|
description:
|
||||||
'Require a password to access admin functions. You are strongly advised to use one for publically accessible instances!',
|
'Require a password to access admin functions. You are strongly advised to use one for publically accessible instances!',
|
||||||
|
flag: '--adminpassword',
|
||||||
metavar: '[password]',
|
metavar: '[password]',
|
||||||
default: 'None',
|
|
||||||
category: 'Administration',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--admindir',
|
category: 'Administration',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Specify a directory to look for .kcpps configs in, which can be used to swap models.',
|
'Specify a directory to look for .kcpps configs in, which can be used to swap models.',
|
||||||
|
flag: '--admindir',
|
||||||
metavar: '[directory]',
|
metavar: '[directory]',
|
||||||
default: '',
|
|
||||||
category: 'Administration',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--hordemodelname',
|
category: 'Horde Worker',
|
||||||
|
default: '',
|
||||||
description: 'Sets your AI Horde display model name.',
|
description: 'Sets your AI Horde display model name.',
|
||||||
|
flag: '--hordemodelname',
|
||||||
metavar: '[name]',
|
metavar: '[name]',
|
||||||
default: '',
|
|
||||||
category: 'Horde Worker',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--hordeworkername',
|
category: 'Horde Worker',
|
||||||
|
default: '',
|
||||||
description: 'Sets your AI Horde worker name.',
|
description: 'Sets your AI Horde worker name.',
|
||||||
|
flag: '--hordeworkername',
|
||||||
metavar: '[name]',
|
metavar: '[name]',
|
||||||
default: '',
|
|
||||||
category: 'Horde Worker',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--hordekey',
|
category: 'Horde Worker',
|
||||||
|
default: '',
|
||||||
description: 'Sets your AI Horde API key.',
|
description: 'Sets your AI Horde API key.',
|
||||||
|
flag: '--hordekey',
|
||||||
metavar: '[apikey]',
|
metavar: '[apikey]',
|
||||||
default: '',
|
|
||||||
category: 'Horde Worker',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--hordemaxctx',
|
category: 'Horde Worker',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Sets the maximum context length your worker will accept from an AI Horde job. If 0, matches main context limit.',
|
'Sets the maximum context length your worker will accept from an AI Horde job. If 0, matches main context limit.',
|
||||||
|
flag: '--hordemaxctx',
|
||||||
metavar: '[amount]',
|
metavar: '[amount]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Horde Worker',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--hordegenlen',
|
category: 'Horde Worker',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Sets the maximum number of tokens your worker will generate from an AI horde job.',
|
'Sets the maximum number of tokens your worker will generate from an AI horde job.',
|
||||||
|
flag: '--hordegenlen',
|
||||||
metavar: '[amount]',
|
metavar: '[amount]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Horde Worker',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdthreads',
|
category: 'Image Generation',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Use a different number of threads for image generation if specified. Otherwise, has the same value as --threads.',
|
'Use a different number of threads for image generation if specified. Otherwise, has the same value as --threads.',
|
||||||
|
flag: '--sdthreads',
|
||||||
metavar: '[threads]',
|
metavar: '[threads]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdquant',
|
category: 'Image Generation',
|
||||||
description: 'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
|
|
||||||
metavar: '[quantization level 0/1/2]',
|
|
||||||
type: 'int',
|
|
||||||
choices: ['0', '1', '2'],
|
choices: ['0', '1', '2'],
|
||||||
default: 0,
|
default: 0,
|
||||||
category: 'Image Generation',
|
description: 'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
|
||||||
|
flag: '--sdquant',
|
||||||
|
metavar: '[quantization level 0/1/2]',
|
||||||
|
type: 'int',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdclamped',
|
category: 'Image Generation',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'If specified, limit generation steps and image size for shared use. Accepts an extra optional parameter that indicates maximum resolution (eg. 768 clamps to 768x768, min 512px, disabled if 0).',
|
'If specified, limit generation steps and image size for shared use. Accepts an extra optional parameter that indicates maximum resolution (eg. 768 clamps to 768x768, min 512px, disabled if 0).',
|
||||||
|
flag: '--sdclamped',
|
||||||
metavar: '[maxres]',
|
metavar: '[maxres]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdclampedsoft',
|
category: 'Image Generation',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'If specified, limit max image size to curb memory usage. Similar to --sdclamped, but less strict, allows trade-offs between width and height (e.g. 640 would allow 640x640, 512x768 and 768x512 images). Total resolution cannot exceed 1MP.',
|
'If specified, limit max image size to curb memory usage. Similar to --sdclamped, but less strict, allows trade-offs between width and height (e.g. 640 would allow 640x640, 512x768 and 768x512 images). Total resolution cannot exceed 1MP.',
|
||||||
|
flag: '--sdclampedsoft',
|
||||||
metavar: '[maxres]',
|
metavar: '[maxres]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdvaeauto',
|
category: 'Image Generation',
|
||||||
description: 'Uses a built-in VAE via TAE SD, which is very fast, and fixed bad VAEs.',
|
description: 'Uses a built-in VAE via TAE SD, which is very fast, and fixed bad VAEs.',
|
||||||
|
flag: '--sdvaeauto',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdupscaler',
|
category: 'Image Generation',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'You can use ESRGAN as an upscaling model to resize images. Leave blank if unused.',
|
'You can use ESRGAN as an upscaling model to resize images. Leave blank if unused.',
|
||||||
|
flag: '--sdupscaler',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdloramult',
|
category: 'Image Generation',
|
||||||
|
default: 1,
|
||||||
description: 'Multiplier for the image LORA model to be applied.',
|
description: 'Multiplier for the image LORA model to be applied.',
|
||||||
|
flag: '--sdloramult',
|
||||||
metavar: '[amount]',
|
metavar: '[amount]',
|
||||||
type: 'float',
|
type: 'float',
|
||||||
default: 1.0,
|
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdtiledvae',
|
category: 'Image Generation',
|
||||||
|
default: 768,
|
||||||
description:
|
description:
|
||||||
'Adjust the automatic VAE tiling trigger for images above this size. 0 disables vae tiling.',
|
'Adjust the automatic VAE tiling trigger for images above this size. 0 disables vae tiling.',
|
||||||
|
flag: '--sdtiledvae',
|
||||||
metavar: '[maxres]',
|
metavar: '[maxres]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 768,
|
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdoffloadcpu',
|
category: 'Image Generation',
|
||||||
description: 'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
|
description: 'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
|
||||||
|
flag: '--sdoffloadcpu',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Image Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--whispermodel',
|
category: 'Audio',
|
||||||
|
default: '',
|
||||||
description: 'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
|
description: 'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
|
||||||
|
flag: '--whispermodel',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Audio',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ttsmodel',
|
category: 'Audio',
|
||||||
|
default: '',
|
||||||
description: 'Specify the TTS Text-To-Speech GGUF model.',
|
description: 'Specify the TTS Text-To-Speech GGUF model.',
|
||||||
|
flag: '--ttsmodel',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Audio',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ttsgpu',
|
category: 'Audio',
|
||||||
description: 'Use the GPU for TTS.',
|
description: 'Use the GPU for TTS.',
|
||||||
|
flag: '--ttsgpu',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Audio',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ttswavtokenizer',
|
category: 'Audio',
|
||||||
|
default: '',
|
||||||
description: 'Specify the WavTokenizer GGUF model.',
|
description: 'Specify the WavTokenizer GGUF model.',
|
||||||
|
flag: '--ttswavtokenizer',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Audio',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ttsdir',
|
category: 'Audio',
|
||||||
|
default: '',
|
||||||
description: 'Select directory containing voices for voice cloning.',
|
description: 'Select directory containing voices for voice cloning.',
|
||||||
|
flag: '--ttsdir',
|
||||||
metavar: '[directory]',
|
metavar: '[directory]',
|
||||||
default: '',
|
|
||||||
category: 'Audio',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ttsmaxlen',
|
category: 'Audio',
|
||||||
description: 'Limit number of audio tokens generated with TTS.',
|
|
||||||
type: 'int',
|
|
||||||
default: 4096,
|
default: 4096,
|
||||||
category: 'Audio',
|
description: 'Limit number of audio tokens generated with TTS.',
|
||||||
|
flag: '--ttsmaxlen',
|
||||||
|
type: 'int',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ttsthreads',
|
category: 'Audio',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Use a different number of threads for TTS if specified. Otherwise, has the same value as --threads.',
|
'Use a different number of threads for TTS if specified. Otherwise, has the same value as --threads.',
|
||||||
|
flag: '--ttsthreads',
|
||||||
metavar: '[threads]',
|
metavar: '[threads]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Audio',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--embeddingsmodel',
|
category: 'Embeddings',
|
||||||
description: 'Specify an embeddings model to be loaded for generating embedding vectors.',
|
|
||||||
metavar: '[filename]',
|
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Embeddings',
|
description: 'Specify an embeddings model to be loaded for generating embedding vectors.',
|
||||||
|
flag: '--embeddingsmodel',
|
||||||
|
metavar: '[filename]',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--embeddingsgpu',
|
category: 'Embeddings',
|
||||||
description: 'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
|
description: 'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
|
||||||
|
flag: '--embeddingsgpu',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Embeddings',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--embeddingsmaxctx',
|
category: 'Embeddings',
|
||||||
|
default: 0,
|
||||||
description:
|
description:
|
||||||
'Overrides the default maximum supported context of an embeddings model (defaults to trained context).',
|
'Overrides the default maximum supported context of an embeddings model (defaults to trained context).',
|
||||||
|
flag: '--embeddingsmaxctx',
|
||||||
metavar: '[amount]',
|
metavar: '[amount]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 0,
|
|
||||||
category: 'Embeddings',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--musicllm',
|
category: 'Music Generation',
|
||||||
|
default: '',
|
||||||
description: 'Select music LLM model (e.g acestep-5Hz-lm-0.6B)',
|
description: 'Select music LLM model (e.g acestep-5Hz-lm-0.6B)',
|
||||||
|
flag: '--musicllm',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Music Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--musicembeddings',
|
category: 'Music Generation',
|
||||||
|
default: '',
|
||||||
description: 'Select music embedding model (e.g Qwen3-Embedding-0.6B)',
|
description: 'Select music embedding model (e.g Qwen3-Embedding-0.6B)',
|
||||||
|
flag: '--musicembeddings',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Music Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--musicdiffusion',
|
category: 'Music Generation',
|
||||||
|
default: '',
|
||||||
description: 'Select music diffusion (DiT) model (e.g acestep-v15-turbo)',
|
description: 'Select music diffusion (DiT) model (e.g acestep-v15-turbo)',
|
||||||
|
flag: '--musicdiffusion',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Music Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--musicvae',
|
category: 'Music Generation',
|
||||||
|
default: '',
|
||||||
description: 'Select music VAE model',
|
description: 'Select music VAE model',
|
||||||
|
flag: '--musicvae',
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
|
||||||
category: 'Music Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--musiclowvram',
|
category: 'Music Generation',
|
||||||
description: 'Unload music models when not in use',
|
description: 'Unload music models when not in use',
|
||||||
|
flag: '--musiclowvram',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Music Generation',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--mcpfile',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Specify path to mcp.json which contains the Cladue Desktop compatible MCP server config.',
|
'Specify path to mcp.json which contains the Cladue Desktop compatible MCP server config.',
|
||||||
|
flag: '--mcpfile',
|
||||||
metavar: '[mcp json file]',
|
metavar: '[mcp json file]',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--device',
|
|
||||||
aliases: ['-dev'],
|
aliases: ['-dev'],
|
||||||
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Set llama.cpp compatible device selection override. Comma separated. Overrides normal device choices.',
|
'Set llama.cpp compatible device selection override. Comma separated. Overrides normal device choices.',
|
||||||
|
flag: '--device',
|
||||||
metavar: '<dev1,dev2,..>',
|
metavar: '<dev1,dev2,..>',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--downloaddir',
|
category: 'Advanced',
|
||||||
|
default: '',
|
||||||
description:
|
description:
|
||||||
'Specify a directory that models will be downloaded to or searched from, if unset uses the working directory.',
|
'Specify a directory that models will be downloaded to or searched from, if unset uses the working directory.',
|
||||||
|
flag: '--downloaddir',
|
||||||
metavar: '[directory]',
|
metavar: '[directory]',
|
||||||
default: '',
|
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--autofitpadding',
|
category: 'Advanced',
|
||||||
description:
|
description:
|
||||||
"How much spare allowance in MB should autofit reserve? If it's too little, the load might fail.",
|
"How much spare allowance in MB should autofit reserve? If it's too little, the load might fail.",
|
||||||
|
flag: '--autofitpadding',
|
||||||
metavar: '[padding in MB]',
|
metavar: '[padding in MB]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
category: 'Advanced',
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const AVAILABLE_ARGUMENTS = COMMAND_LINE_ARGUMENTS.filter(
|
const AVAILABLE_ARGUMENTS = COMMAND_LINE_ARGUMENTS.filter(
|
||||||
(arg) => !UI_COVERED_ARGS.has(arg.flag) && !IGNORED_ARGS.has(arg.flag)
|
(arg) => !UI_COVERED_ARGS.has(arg.flag) && !IGNORED_ARGS.has(arg.flag),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CommandLineArgumentsModal = ({
|
export const CommandLineArgumentsModal = ({
|
||||||
|
|
@ -782,7 +783,7 @@ export const CommandLineArgumentsModal = ({
|
||||||
}: CommandLineArgumentsModalProps) => {
|
}: CommandLineArgumentsModalProps) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const argumentsByCategory = AVAILABLE_ARGUMENTS.reduce(
|
const argumentsByCategory = AVAILABLE_ARGUMENTS.reduce<Record<string, ArgumentInfo[]>>(
|
||||||
(acc, arg) => {
|
(acc, arg) => {
|
||||||
if (!acc[arg.category]) {
|
if (!acc[arg.category]) {
|
||||||
acc[arg.category] = [];
|
acc[arg.category] = [];
|
||||||
|
|
@ -790,26 +791,25 @@ export const CommandLineArgumentsModal = ({
|
||||||
acc[arg.category].push(arg);
|
acc[arg.category].push(arg);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, ArgumentInfo[]>
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredCategories = Object.entries(argumentsByCategory).reduce(
|
const filteredCategories = Object.entries(argumentsByCategory).reduce<
|
||||||
(acc, [category, args]) => {
|
Record<string, ArgumentInfo[]>
|
||||||
const filteredArgs = args.filter(
|
>((acc, [category, args]) => {
|
||||||
(arg) =>
|
const filteredArgs = args.filter(
|
||||||
arg.flag.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(arg) =>
|
||||||
arg.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
arg.flag.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
arg.aliases?.some((alias) => alias.toLowerCase().includes(searchQuery.toLowerCase()))
|
arg.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
);
|
arg.aliases?.some((alias) => alias.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||||
|
);
|
||||||
|
|
||||||
if (filteredArgs.length > 0) {
|
if (filteredArgs.length > 0) {
|
||||||
acc[category] = filteredArgs;
|
acc[category] = filteredArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, {});
|
||||||
{} as Record<string, ArgumentInfo[]>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddArgument = (arg: ArgumentInfo) => {
|
const handleAddArgument = (arg: ArgumentInfo) => {
|
||||||
if (onAddArgument) {
|
if (onAddArgument) {
|
||||||
|
|
@ -896,7 +896,7 @@ export const CommandLineArgumentsModal = ({
|
||||||
{arg.description}
|
{arg.description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{(arg.metavar || arg.default !== undefined || arg.choices) && (
|
{((arg.metavar ?? arg.default !== undefined) || arg.choices) && (
|
||||||
<Group gap="lg">
|
<Group gap="lg">
|
||||||
{arg.metavar && (
|
{arg.metavar && (
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Button, Group, Stack, Text, Tooltip } from '@mantine/core';
|
import { Button, Group, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { Check, File, Plus, Save, Trash2 } from 'lucide-react';
|
import { Check, File, Plus, Save, Trash2 } from 'lucide-react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
import { stripFileExtension } from '@/utils/format';
|
import { stripFileExtension } from '@/utils/format';
|
||||||
|
|
||||||
import { CreateConfigModal } from './CreateConfigModal';
|
import { CreateConfigModal } from './CreateConfigModal';
|
||||||
import { DeleteConfigModal } from './DeleteConfigModal';
|
import { DeleteConfigModal } from './DeleteConfigModal';
|
||||||
|
|
||||||
|
|
@ -31,7 +33,7 @@ export const ConfigFileManager = ({
|
||||||
const [configToDelete, setConfigToDelete] = useState<string | null>(null);
|
const [configToDelete, setConfigToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const existingConfigNames = configFiles.map((file) =>
|
const existingConfigNames = configFiles.map((file) =>
|
||||||
stripFileExtension(file.name).toLowerCase()
|
stripFileExtension(file.name).toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenConfigModal = () => {
|
const handleOpenConfigModal = () => {
|
||||||
|
|
@ -77,13 +79,13 @@ export const ConfigFileManager = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectData = configFiles.map((file) => {
|
const selectData = configFiles.map((file) => {
|
||||||
const extension = file.name.split('.').pop() || '';
|
const extension = file.name.split('.').pop() ?? '';
|
||||||
const nameWithoutExtension = stripFileExtension(file.name);
|
const nameWithoutExtension = stripFileExtension(file.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: file.name,
|
|
||||||
label: nameWithoutExtension,
|
|
||||||
extension: `.${extension}`,
|
extension: `.${extension}`,
|
||||||
|
label: nameWithoutExtension,
|
||||||
|
value: file.name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -138,7 +140,7 @@ export const ConfigFileManager = ({
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
disabled={!selectedFile}
|
disabled={!selectedFile}
|
||||||
color="red"
|
color="red"
|
||||||
style={{ width: '2.5rem', padding: '0 0.5rem' }}
|
style={{ padding: '0 0.5rem', width: '2.5rem' }}
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Button, Group, Stack, TextInput } from '@mantine/core';
|
import { Button, Group, Stack, TextInput } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface CreateConfigModalProps {
|
interface CreateConfigModalProps {
|
||||||
|
|
@ -49,7 +50,7 @@ export const CreateConfigModal = ({
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!trimmedConfigName || !!configNameExists}
|
disabled={!trimmedConfigName || Boolean(configNameExists)}
|
||||||
onClick={() => void handleSubmit()}
|
onClick={() => void handleSubmit()}
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { stripFileExtension } from '@/utils/format';
|
import { stripFileExtension } from '@/utils/format';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,53 @@
|
||||||
import { Badge, Box, Group, Text } from '@mantine/core';
|
import { Badge, Box, Group, Text } from '@mantine/core';
|
||||||
|
|
||||||
import type { AccelerationOption } from '@/types';
|
import type { AccelerationOption } from '@/types';
|
||||||
import type { GPUDevice } from '@/types/hardware';
|
import type { GPUDevice } from '@/types/hardware';
|
||||||
|
|
||||||
type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
|
type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
|
||||||
|
|
||||||
|
const renderDeviceName = (device: string | GPUDevice) => {
|
||||||
|
const deviceName = typeof device === 'string' ? device : device.name;
|
||||||
|
return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` : deviceName;
|
||||||
|
};
|
||||||
|
|
||||||
export const AccelerationSelectItem = ({
|
export const AccelerationSelectItem = ({
|
||||||
label,
|
label,
|
||||||
devices,
|
devices,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: AccelerationSelectItemProps) => {
|
}: AccelerationSelectItemProps) => (
|
||||||
const renderDeviceName = (device: string | GPUDevice) => {
|
<Group justify="space-between" wrap="nowrap">
|
||||||
const deviceName = typeof device === 'string' ? device : device.name;
|
<Box w={!disabled ? '3.5rem' : 'auto'}>
|
||||||
return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` : deviceName;
|
<Text size="sm" truncate>
|
||||||
};
|
{label}
|
||||||
|
{disabled && (
|
||||||
return (
|
<Text component="span" size="xs" ml="xs">
|
||||||
<Group justify="space-between" wrap="nowrap">
|
(Compatible devices not found)
|
||||||
<Box w={!disabled ? '3.5rem' : 'auto'}>
|
</Text>
|
||||||
<Text size="sm" truncate>
|
)}
|
||||||
{label}
|
</Text>
|
||||||
{disabled && (
|
</Box>
|
||||||
<Text component="span" size="xs" ml="xs">
|
{devices &&
|
||||||
(Compatible devices not found)
|
devices.length > 0 &&
|
||||||
</Text>
|
(() => {
|
||||||
)}
|
const discreteDevices = devices.filter(
|
||||||
</Text>
|
(device) => typeof device === 'string' || !device.isIntegrated,
|
||||||
</Box>
|
);
|
||||||
{devices &&
|
return (
|
||||||
devices.length > 0 &&
|
discreteDevices.length > 0 && (
|
||||||
(() => {
|
<Group gap={4}>
|
||||||
const discreteDevices = devices.filter(
|
{discreteDevices.slice(0, 2).map((device, index) => (
|
||||||
(device) => typeof device === 'string' || !device.isIntegrated
|
<Badge key={index} size="md" variant="light" color="blue">
|
||||||
);
|
{renderDeviceName(device)}
|
||||||
return (
|
</Badge>
|
||||||
discreteDevices.length > 0 && (
|
))}
|
||||||
<Group gap={4}>
|
{discreteDevices.length > 2 && (
|
||||||
{discreteDevices.slice(0, 2).map((device, index) => (
|
<Badge size="md" variant="light" color="gray">
|
||||||
<Badge key={index} size="md" variant="light" color="blue">
|
+{discreteDevices.length - 2}
|
||||||
{renderDeviceName(device)}
|
</Badge>
|
||||||
</Badge>
|
)}
|
||||||
))}
|
</Group>
|
||||||
{discreteDevices.length > 2 && (
|
)
|
||||||
<Badge size="md" variant="light" color="gray">
|
);
|
||||||
+{discreteDevices.length - 2}
|
})()}
|
||||||
</Badge>
|
</Group>
|
||||||
)}
|
);
|
||||||
</Group>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Checkbox, Group, Text, TextInput } from '@mantine/core';
|
import { Checkbox, Group, Text, TextInput } from '@mantine/core';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { Select } from '@/components/Select';
|
|
||||||
import { AccelerationSelectItem } from '@/components/screens/Launch/GeneralTab/AccelerationSelectItem';
|
import { AccelerationSelectItem } from '@/components/screens/Launch/GeneralTab/AccelerationSelectItem';
|
||||||
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
||||||
|
import { Select } from '@/components/Select';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import type { Acceleration, AccelerationOption } from '@/types';
|
import type { Acceleration, AccelerationOption } from '@/types';
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ export const AccelerationSelector = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (availableAccelerations.length > 0 && acceleration) {
|
if (availableAccelerations.length > 0 && acceleration) {
|
||||||
const isAccelerationAvailable = availableAccelerations.some(
|
const isAccelerationAvailable = availableAccelerations.some(
|
||||||
(a) => a.value === acceleration && !a.disabled
|
(a) => a.value === acceleration && !a.disabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAccelerationAvailable) {
|
if (!isAccelerationAvailable) {
|
||||||
|
|
@ -104,7 +105,7 @@ export const AccelerationSelector = () => {
|
||||||
contextSize,
|
contextSize,
|
||||||
availableVramGB,
|
availableVramGB,
|
||||||
flashattention,
|
flashattention,
|
||||||
acceleration
|
acceleration,
|
||||||
);
|
);
|
||||||
|
|
||||||
setGpuLayers(result.recommendedLayers);
|
setGpuLayers(result.recommendedLayers);
|
||||||
|
|
@ -153,9 +154,9 @@ export const AccelerationSelector = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data={availableAccelerations.map((a) => ({
|
data={availableAccelerations.map((a) => ({
|
||||||
value: a.value,
|
|
||||||
label: a.label,
|
|
||||||
disabled: a.disabled,
|
disabled: a.disabled,
|
||||||
|
label: a.label,
|
||||||
|
value: a.value,
|
||||||
}))}
|
}))}
|
||||||
disabled={isLoadingAccelerations || availableAccelerations.length === 0}
|
disabled={isLoadingAccelerations || availableAccelerations.length === 0}
|
||||||
renderOption={({ option }) => {
|
renderOption={({ option }) => {
|
||||||
|
|
@ -163,7 +164,7 @@ export const AccelerationSelector = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccelerationSelectItem
|
<AccelerationSelectItem
|
||||||
label={accelerationData?.label || option.label.split(' (')[0]}
|
label={accelerationData?.label ?? option.label.split(' (')[0]}
|
||||||
devices={accelerationData?.devices}
|
devices={accelerationData?.devices}
|
||||||
disabled={accelerationData?.disabled}
|
disabled={accelerationData?.disabled}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { Group, Text, TextInput } from '@mantine/core';
|
import { Group, Text, TextInput } from '@mantine/core';
|
||||||
|
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import type { AccelerationOption } from '@/types';
|
import type { AccelerationOption } from '@/types';
|
||||||
|
|
||||||
const GPU_ACCELERATIONS = ['cuda', 'rocm', 'vulkan'];
|
const GPU_ACCELERATIONS = new Set(['cuda', 'rocm', 'vulkan']);
|
||||||
const TENSOR_SPLIT_ACCELERATIONS = ['cuda', 'rocm', 'vulkan'];
|
const TENSOR_SPLIT_ACCELERATIONS = new Set(['cuda', 'rocm', 'vulkan']);
|
||||||
|
|
||||||
interface GpuDeviceSelectorProps {
|
interface GpuDeviceSelectorProps {
|
||||||
availableAccelerations: AccelerationOption[];
|
availableAccelerations: AccelerationOption[];
|
||||||
|
|
@ -16,13 +17,15 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
|
||||||
useLaunchConfigStore();
|
useLaunchConfigStore();
|
||||||
|
|
||||||
const selectedAcceleration = availableAccelerations.find((a) => a.value === acceleration);
|
const selectedAcceleration = availableAccelerations.find((a) => a.value === acceleration);
|
||||||
const isGpuAcceleration = GPU_ACCELERATIONS.includes(acceleration);
|
const isGpuAcceleration = GPU_ACCELERATIONS.has(acceleration);
|
||||||
|
|
||||||
const getDiscreteDeviceCount = () => {
|
const getDiscreteDeviceCount = () => {
|
||||||
if (!selectedAcceleration?.devices) return 0;
|
if (!selectedAcceleration?.devices) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (acceleration === 'vulkan' || acceleration === 'rocm') {
|
if (acceleration === 'vulkan' || acceleration === 'rocm') {
|
||||||
return selectedAcceleration.devices.filter(
|
return selectedAcceleration.devices.filter(
|
||||||
(device) => typeof device === 'string' || !device.isIntegrated
|
(device) => typeof device === 'string' || !device.isIntegrated,
|
||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
return selectedAcceleration.devices.length;
|
return selectedAcceleration.devices.length;
|
||||||
|
|
@ -30,7 +33,7 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
|
||||||
|
|
||||||
const hasMultipleDevices = getDiscreteDeviceCount() > 1;
|
const hasMultipleDevices = getDiscreteDeviceCount() > 1;
|
||||||
const showTensorSplit =
|
const showTensorSplit =
|
||||||
TENSOR_SPLIT_ACCELERATIONS.includes(acceleration) &&
|
TENSOR_SPLIT_ACCELERATIONS.has(acceleration) &&
|
||||||
hasMultipleDevices &&
|
hasMultipleDevices &&
|
||||||
gpuDeviceSelection === 'all';
|
gpuDeviceSelection === 'all';
|
||||||
|
|
||||||
|
|
@ -39,7 +42,9 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceOptions = (() => {
|
const deviceOptions = (() => {
|
||||||
if (!selectedAcceleration?.devices) return [];
|
if (!selectedAcceleration?.devices) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (acceleration === 'vulkan' || acceleration === 'rocm') {
|
if (acceleration === 'vulkan' || acceleration === 'rocm') {
|
||||||
const discreteDeviceOptions = selectedAcceleration.devices
|
const discreteDeviceOptions = selectedAcceleration.devices
|
||||||
|
|
@ -49,17 +54,17 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
|
||||||
}
|
}
|
||||||
const deviceName = typeof device === 'string' ? device : device.name;
|
const deviceName = typeof device === 'string' ? device : device.name;
|
||||||
return {
|
return {
|
||||||
value: index.toString(),
|
|
||||||
label: `GPU ${index}: ${deviceName}`,
|
label: `GPU ${index}: ${deviceName}`,
|
||||||
|
value: index.toString(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((option): option is NonNullable<typeof option> => option !== null);
|
.filter((option): option is NonNullable<typeof option> => option !== null);
|
||||||
|
|
||||||
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions];
|
return [{ label: 'All GPUs', value: 'all' }, ...discreteDeviceOptions];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ value: 'all', label: 'All GPUs' },
|
{ label: 'All GPUs', value: 'all' },
|
||||||
...selectedAcceleration.devices.map((device, index) => {
|
...selectedAcceleration.devices.map((device, index) => {
|
||||||
const deviceName =
|
const deviceName =
|
||||||
typeof device === 'string'
|
typeof device === 'string'
|
||||||
|
|
@ -68,8 +73,8 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
|
||||||
? device.name
|
? device.name
|
||||||
: String(device);
|
: String(device);
|
||||||
return {
|
return {
|
||||||
value: index.toString(),
|
|
||||||
label: `GPU ${index}: ${deviceName}`,
|
label: `GPU ${index}: ${deviceName}`,
|
||||||
|
value: index.toString(),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Group, Slider, Stack, Text, TextInput, Tooltip, Transition } from '@mantine/core';
|
import { Group, Slider, Stack, Text, TextInput, Tooltip, Transition } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector';
|
import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector';
|
||||||
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
||||||
|
|
@ -44,8 +45,8 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
onChange={setModel}
|
onChange={setModel}
|
||||||
onSelectFile={() => void selectFile('model', 'Select Text Model')}
|
onSelectFile={() => void selectFile('model', 'Select Text Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
pipelineTag: 'text-generation',
|
|
||||||
filter: 'gguf',
|
filter: 'gguf',
|
||||||
|
pipelineTag: 'text-generation',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
showAnalyze
|
showAnalyze
|
||||||
|
|
@ -65,7 +66,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
onChange={(event) => setContextSizeWithStep(Number(event.target.value) || 256)}
|
onChange={(event) => setContextSizeWithStep(Number(event.target.value) || 256)}
|
||||||
type="number"
|
type="number"
|
||||||
min={256}
|
min={256}
|
||||||
max={262144}
|
max={262_144}
|
||||||
step={256}
|
step={256}
|
||||||
size="sm"
|
size="sm"
|
||||||
w={100}
|
w={100}
|
||||||
|
|
@ -75,7 +76,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
<Slider
|
<Slider
|
||||||
value={contextSize}
|
value={contextSize}
|
||||||
min={256}
|
min={256}
|
||||||
max={262144}
|
max={262_144}
|
||||||
step={256}
|
step={256}
|
||||||
onChange={setContextSizeWithStep}
|
onChange={setContextSizeWithStep}
|
||||||
label={null}
|
label={null}
|
||||||
|
|
@ -87,7 +88,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
withinPortal
|
withinPortal
|
||||||
opened={isSliding ? true : undefined}
|
opened={isSliding ? true : undefined}
|
||||||
>
|
>
|
||||||
<span style={{ display: 'block', width: '100%', height: '100%' }} />
|
<span style={{ display: 'block', height: '100%', width: '100%' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Group, Loader, Table, Text } from '@mantine/core';
|
import { Group, Loader, Table, Text } from '@mantine/core';
|
||||||
|
|
||||||
import type { HuggingFaceFileInfo } from '@/types';
|
import type { HuggingFaceFileInfo } from '@/types';
|
||||||
import { formatBytes } from '@/utils/format';
|
import { formatBytes } from '@/utils/format';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import rehypeSanitize from 'rehype-sanitize';
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
interface ModelCardProps {
|
interface ModelCardProps {
|
||||||
|
|
@ -16,7 +17,9 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadReadme = async () => {
|
const loadReadme = async () => {
|
||||||
if (readme !== null) return;
|
if (readme !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
|
@ -57,15 +60,23 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
p="md"
|
p="md"
|
||||||
withBorder
|
withBorder
|
||||||
style={{
|
style={{
|
||||||
overflow: 'auto',
|
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
|
overflow: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||||
components={{
|
components={{
|
||||||
img: ({ node, ...props }) => (
|
code: ({ node: _, ...props }) => (
|
||||||
|
<code
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
img: ({ node: _, ...props }) => (
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -75,7 +86,7 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
pre: ({ node, ...props }) => (
|
pre: ({ node: _, ...props }) => (
|
||||||
<pre
|
<pre
|
||||||
{...props}
|
{...props}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -84,14 +95,6 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
code: ({ node, ...props }) => (
|
|
||||||
<code
|
|
||||||
{...props}
|
|
||||||
style={{
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{readme}
|
{readme}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Badge, Group, Loader, Stack, Table, Text, Tooltip } from '@mantine/core';
|
import { Badge, Group, Loader, Stack, Table, Text, Tooltip } from '@mantine/core';
|
||||||
import { Download, Heart, Lock } from 'lucide-react';
|
import { Download, Heart, Lock } from 'lucide-react';
|
||||||
|
|
||||||
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
|
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
|
||||||
import { formatDate, formatDownloads } from '@/utils/format';
|
import { formatDate, formatDownloads } from '@/utils/format';
|
||||||
|
|
@ -42,12 +43,12 @@ export const ModelsTable = ({
|
||||||
<Table.Th>Model</Table.Th>
|
<Table.Th>Model</Table.Th>
|
||||||
<Table.Th style={{ width: 80 }}>Params</Table.Th>
|
<Table.Th style={{ width: 80 }}>Params</Table.Th>
|
||||||
<Table.Th
|
<Table.Th
|
||||||
style={{ width: 120, cursor: 'pointer' }}
|
style={{ cursor: 'pointer', width: 120 }}
|
||||||
onClick={() => onSortChange('downloads')}
|
onClick={() => onSortChange('downloads')}
|
||||||
>
|
>
|
||||||
Downloads{sortBy === 'downloads' && ' ↓'}
|
Downloads{sortBy === 'downloads' && ' ↓'}
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
<Table.Th style={{ width: 90, cursor: 'pointer' }} onClick={() => onSortChange('likes')}>
|
<Table.Th style={{ cursor: 'pointer', width: 90 }} onClick={() => onSortChange('likes')}>
|
||||||
Likes{sortBy === 'likes' && ' ↓'}
|
Likes{sortBy === 'likes' && ' ↓'}
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { ArrowLeft, ExternalLink, Search } from 'lucide-react';
|
import { ArrowLeft, ExternalLink, Search } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
|
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
|
||||||
|
|
@ -23,6 +24,7 @@ import type {
|
||||||
HuggingFaceSearchParams,
|
HuggingFaceSearchParams,
|
||||||
HuggingFaceSortOption,
|
HuggingFaceSortOption,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
import { FilesTable } from './FilesTable';
|
import { FilesTable } from './FilesTable';
|
||||||
import { ModelCard } from './ModelCard';
|
import { ModelCard } from './ModelCard';
|
||||||
import { ModelsTable } from './ModelsTable';
|
import { ModelsTable } from './ModelsTable';
|
||||||
|
|
@ -40,7 +42,7 @@ export const HuggingFaceSearchModal = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
searchParams: initialSearchParams,
|
searchParams: initialSearchParams,
|
||||||
}: HuggingFaceSearchModalProps) => {
|
}: HuggingFaceSearchModalProps) => {
|
||||||
const [searchQuery, setSearchQuery] = useState(initialSearchParams.search || '');
|
const [searchQuery, setSearchQuery] = useState(initialSearchParams.search ?? '');
|
||||||
const [debouncedQuery] = useDebouncedValue(searchQuery, 600);
|
const [debouncedQuery] = useDebouncedValue(searchQuery, 600);
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
const prevOpenedRef = useRef(false);
|
const prevOpenedRef = useRef(false);
|
||||||
|
|
@ -84,7 +86,7 @@ export const HuggingFaceSearchModal = ({
|
||||||
}, [debouncedQuery, opened, searchModels]);
|
}, [debouncedQuery, opened, searchModels]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSearchQuery(initialSearchParams.search || '');
|
setSearchQuery(initialSearchParams.search ?? '');
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -93,7 +95,9 @@ export const HuggingFaceSearchModal = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (file: HuggingFaceFileInfo) => {
|
const handleFileSelect = (file: HuggingFaceFileInfo) => {
|
||||||
if (!selectedModel) return;
|
if (!selectedModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = getFileDownloadUrl(selectedModel.id, file.path);
|
const url = getFileDownloadUrl(selectedModel.id, file.path);
|
||||||
onSelect(url);
|
onSelect(url);
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
@ -112,8 +116,12 @@ export const HuggingFaceSearchModal = ({
|
||||||
|
|
||||||
const getFilterBadges = () => {
|
const getFilterBadges = () => {
|
||||||
const badges: string[] = [];
|
const badges: string[] = [];
|
||||||
if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase());
|
if (searchParams?.filter) {
|
||||||
if (searchParams?.pipelineTag) badges.push(searchParams.pipelineTag.replace('-', ' '));
|
badges.push(searchParams.filter.toUpperCase());
|
||||||
|
}
|
||||||
|
if (searchParams?.pipelineTag) {
|
||||||
|
badges.push(searchParams.pipelineTag.replace('-', ' '));
|
||||||
|
}
|
||||||
return badges;
|
return badges;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -187,7 +195,9 @@ export const HuggingFaceSearchModal = ({
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onScrollPositionChange={({ y }) => {
|
onScrollPositionChange={({ y }) => {
|
||||||
const target = scrollAreaRef.current;
|
const target = scrollAreaRef.current;
|
||||||
if (!target) return;
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const isNearBottom = target.scrollHeight - y <= target.clientHeight + 100;
|
const isNearBottom = target.scrollHeight - y <= target.clientHeight + 100;
|
||||||
if (isNearBottom && hasMore && !loading && !selectedModel) {
|
if (isNearBottom && hasMore && !loading && !selectedModel) {
|
||||||
void loadMoreModels();
|
void loadMoreModels();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
|
|
||||||
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
||||||
|
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
|
||||||
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
|
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
||||||
|
|
@ -41,8 +42,8 @@ export const ImageGenerationTab = () => {
|
||||||
tooltip="Quick presets for popular image generation models with pre-configured encoders."
|
tooltip="Quick presets for popular image generation models with pre-configured encoders."
|
||||||
placeholder="Choose a preset..."
|
placeholder="Choose a preset..."
|
||||||
data={IMAGE_MODEL_PRESETS.map((preset) => ({
|
data={IMAGE_MODEL_PRESETS.map((preset) => ({
|
||||||
value: preset.name,
|
|
||||||
label: preset.name,
|
label: preset.name,
|
||||||
|
value: preset.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedPreset ?? ''}
|
value={selectedPreset ?? ''}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
|
@ -70,8 +71,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdmodel}
|
onChange={setSdmodel}
|
||||||
onSelectFile={() => void selectFile('sdmodel', 'Select Image Model')}
|
onSelectFile={() => void selectFile('sdmodel', 'Select Image Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
pipelineTag: 'text-to-image',
|
|
||||||
filter: 'gguf',
|
filter: 'gguf',
|
||||||
|
pipelineTag: 'text-to-image',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
showAnalyze
|
showAnalyze
|
||||||
|
|
@ -86,8 +87,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdt5xxl}
|
onChange={setSdt5xxl}
|
||||||
onSelectFile={() => void selectFile('sdt5xxl', 'Select T5XXL Model')}
|
onSelectFile={() => void selectFile('sdt5xxl', 'Select T5XXL Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 't5xxl',
|
|
||||||
filter: 'safetensors',
|
filter: 'safetensors',
|
||||||
|
search: 't5xxl',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
paramType="sdt5xxl"
|
paramType="sdt5xxl"
|
||||||
|
|
@ -101,8 +102,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdclipl}
|
onChange={setSdclipl}
|
||||||
onSelectFile={() => void selectFile('sdclipl', 'Select CLIP-L Model')}
|
onSelectFile={() => void selectFile('sdclipl', 'Select CLIP-L Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 'clip',
|
|
||||||
filter: 'safetensors',
|
filter: 'safetensors',
|
||||||
|
search: 'clip',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
paramType="sdclipl"
|
paramType="sdclipl"
|
||||||
|
|
@ -116,8 +117,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdclipg}
|
onChange={setSdclipg}
|
||||||
onSelectFile={() => void selectFile('sdclipg', 'Select CLIP-G Model')}
|
onSelectFile={() => void selectFile('sdclipg', 'Select CLIP-G Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 'clip',
|
|
||||||
filter: 'gguf',
|
filter: 'gguf',
|
||||||
|
search: 'clip',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
paramType="sdclipg"
|
paramType="sdclipg"
|
||||||
|
|
@ -131,8 +132,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdphotomaker}
|
onChange={setSdphotomaker}
|
||||||
onSelectFile={() => void selectFile('sdphotomaker', 'Select PhotoMaker Model')}
|
onSelectFile={() => void selectFile('sdphotomaker', 'Select PhotoMaker Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 'photomaker',
|
|
||||||
filter: 'safetensors',
|
filter: 'safetensors',
|
||||||
|
search: 'photomaker',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
paramType="sdphotomaker"
|
paramType="sdphotomaker"
|
||||||
|
|
@ -146,8 +147,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdvae}
|
onChange={setSdvae}
|
||||||
onSelectFile={() => void selectFile('sdvae', 'Select VAE Model')}
|
onSelectFile={() => void selectFile('sdvae', 'Select VAE Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 'vae',
|
|
||||||
filter: 'safetensors',
|
filter: 'safetensors',
|
||||||
|
search: 'vae',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
paramType="sdvae"
|
paramType="sdvae"
|
||||||
|
|
@ -161,8 +162,8 @@ export const ImageGenerationTab = () => {
|
||||||
onChange={setSdlora}
|
onChange={setSdlora}
|
||||||
onSelectFile={() => void selectFile('sdlora', 'Select LoRA Model')}
|
onSelectFile={() => void selectFile('sdlora', 'Select LoRA Model')}
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 'lora',
|
|
||||||
filter: 'safetensors',
|
filter: 'safetensors',
|
||||||
|
search: 'lora',
|
||||||
sort: 'trendingScore',
|
sort: 'trendingScore',
|
||||||
}}
|
}}
|
||||||
paramType="sdlora"
|
paramType="sdlora"
|
||||||
|
|
@ -178,9 +179,9 @@ export const ImageGenerationTab = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data={[
|
data={[
|
||||||
{ value: 'off', label: 'Off' },
|
{ label: 'Off', value: 'off' },
|
||||||
{ value: 'vaeonly', label: 'VAE Only' },
|
{ label: 'VAE Only', value: 'vaeonly' },
|
||||||
{ value: 'full', label: 'Full' },
|
{ label: 'Full', value: 'full' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Alert, Group, rem, Stack, Text } from '@mantine/core';
|
import { Alert, Group, Stack, Text, rem } from '@mantine/core';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import type { ModelAnalysis } from '@/types';
|
import type { ModelAnalysis } from '@/types';
|
||||||
|
|
||||||
|
|
@ -17,7 +18,9 @@ interface InfoRowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfoRow = ({ label, value }: InfoRowProps) => {
|
const InfoRow = ({ label, value }: InfoRowProps) => {
|
||||||
if (!value) return null;
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group gap="md" wrap="nowrap">
|
<Group gap="md" wrap="nowrap">
|
||||||
|
|
@ -79,7 +82,7 @@ export const ModelAnalysisModal = ({
|
||||||
{analysis.architecture.layers && (
|
{analysis.architecture.layers && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Full VRAM"
|
label="Full VRAM"
|
||||||
value={`${analysis.architecture.layers} x ${analysis.estimates.vramPerLayer || 'N/A'} per layer = ${analysis.estimates.fullGpuVram}`}
|
value={`${analysis.architecture.layers} x ${analysis.estimates.vramPerLayer ?? 'N/A'} per layer = ${analysis.estimates.fullGpuVram}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!analysis.architecture.layers && (
|
{!analysis.architecture.layers && (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { File, Info, Search } from 'lucide-react';
|
import { File, Info, Search } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
||||||
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
|
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
|
||||||
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
|
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
|
||||||
|
|
@ -66,7 +67,9 @@ export const ModelFileField = ({
|
||||||
));
|
));
|
||||||
|
|
||||||
const getHelperText = () => {
|
const getHelperText = () => {
|
||||||
if (validationState === 'neutral') return undefined;
|
if (validationState === 'neutral') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (validationState === 'invalid') {
|
if (validationState === 'invalid') {
|
||||||
return 'Enter a valid URL or file path';
|
return 'Enter a valid URL or file path';
|
||||||
|
|
@ -76,7 +79,9 @@ export const ModelFileField = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAnalyzeModel = async () => {
|
const handleAnalyzeModel = async () => {
|
||||||
if (validationState === 'neutral' || validationState === 'invalid') return;
|
if (validationState === 'neutral' || validationState === 'invalid') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setAnalysisModalOpened(true);
|
setAnalysisModalOpened(true);
|
||||||
setAnalysisLoading(true);
|
setAnalysisLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Group, Stack, Text, TextInput } from '@mantine/core';
|
import { Group, Stack, Text, TextInput } from '@mantine/core';
|
||||||
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
@ -49,7 +50,7 @@ export const NetworkTab = () => {
|
||||||
placeholder="5001"
|
placeholder="5001"
|
||||||
value={port?.toString() ?? ''}
|
value={port?.toString() ?? ''}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const value = event.currentTarget.value;
|
const { value } = event.currentTarget;
|
||||||
|
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
setPort(undefined);
|
setPort(undefined);
|
||||||
|
|
@ -57,13 +58,13 @@ export const NetworkTab = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const numValue = Number(value);
|
const numValue = Number(value);
|
||||||
if (!Number.isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
|
if (!Number.isNaN(numValue) && numValue >= 1 && numValue <= 65_535) {
|
||||||
setPort(numValue);
|
setPort(numValue);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65_535}
|
||||||
w={120}
|
w={120}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Group, NumberInput, SimpleGrid, Stack, Text } from '@mantine/core';
|
import { Group, NumberInput, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||||
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Button, Card, Container, Group, Stack, Tabs } from '@mantine/core';
|
import { Button, Card, Container, Group, Stack, Tabs } from '@mantine/core';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
|
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
|
||||||
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
|
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
|
||||||
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
|
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
|
||||||
|
|
@ -74,15 +75,15 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
|
|
||||||
const { isLaunching, handleLaunch } = useLaunchLogic({
|
const { isLaunching, handleLaunch } = useLaunchLogic({
|
||||||
model,
|
model,
|
||||||
sdmodel,
|
|
||||||
onLaunch,
|
onLaunch,
|
||||||
|
sdmodel,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { warnings: combinedWarnings } = useWarnings({
|
const { warnings: combinedWarnings } = useWarnings({
|
||||||
model,
|
|
||||||
sdmodel,
|
|
||||||
acceleration,
|
acceleration,
|
||||||
configLoaded,
|
configLoaded,
|
||||||
|
model,
|
||||||
|
sdmodel,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setHappyDefaults = useCallback(async () => {
|
const setHappyDefaults = useCallback(async () => {
|
||||||
|
|
@ -100,14 +101,14 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
defaultsSetRef.current = true;
|
defaultsSetRef.current = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setModel]
|
[setModel],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (configLoaded && !defaultsSetRef.current) {
|
if (configLoaded && !defaultsSetRef.current) {
|
||||||
void setHappyDefaults();
|
void setHappyDefaults();
|
||||||
if (!model.trim() && !sdmodel.trim()) {
|
if (!model.trim() && !sdmodel.trim()) {
|
||||||
void setInitialDefaults(model, sdmodel);
|
setInitialDefaults(model, sdmodel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [configLoaded, setHappyDefaults, model, sdmodel, setInitialDefaults]);
|
}, [configLoaded, setHappyDefaults, model, sdmodel, setInitialDefaults]);
|
||||||
|
|
@ -147,50 +148,50 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildConfigData = () => ({
|
const buildConfigData = () => ({
|
||||||
autoGpuLayers,
|
|
||||||
gpulayers: gpuLayers,
|
|
||||||
contextsize: contextSize,
|
|
||||||
model,
|
|
||||||
additionalArguments,
|
additionalArguments,
|
||||||
preLaunchCommands: preLaunchCommands.filter((cmd) => cmd.trim() !== ''),
|
autoGpuLayers,
|
||||||
port,
|
contextsize: contextSize,
|
||||||
host,
|
|
||||||
multiuser: multiuser ? 1 : 0,
|
|
||||||
multiplayer,
|
|
||||||
remotetunnel,
|
|
||||||
nocertify,
|
|
||||||
websearch,
|
|
||||||
noshift,
|
|
||||||
flashattention,
|
|
||||||
noavx2,
|
|
||||||
failsafe,
|
|
||||||
usemmap,
|
|
||||||
debugmode,
|
debugmode,
|
||||||
|
failsafe,
|
||||||
|
flashattention,
|
||||||
|
gpuDeviceSelection,
|
||||||
|
gpulayers: gpuLayers,
|
||||||
|
host,
|
||||||
|
model,
|
||||||
moecpu,
|
moecpu,
|
||||||
moeexperts,
|
moeexperts,
|
||||||
smartcache,
|
multiplayer,
|
||||||
|
multiuser: multiuser ? 1 : 0,
|
||||||
|
noavx2,
|
||||||
|
nocertify,
|
||||||
|
noshift,
|
||||||
pipelineparallel,
|
pipelineparallel,
|
||||||
usecuda: acceleration === 'cuda' || acceleration === 'rocm',
|
port,
|
||||||
usevulkan: acceleration === 'vulkan',
|
preLaunchCommands: preLaunchCommands.filter((cmd) => cmd.trim() !== ''),
|
||||||
gpuDeviceSelection,
|
remotetunnel,
|
||||||
tensorSplit,
|
|
||||||
sdmodel,
|
|
||||||
sdt5xxl,
|
|
||||||
sdclipl,
|
|
||||||
sdclipg,
|
sdclipg,
|
||||||
sdphotomaker,
|
|
||||||
sdvae,
|
|
||||||
sdlora,
|
|
||||||
sdconvdirect,
|
|
||||||
sdvaecpu,
|
|
||||||
sdclipgpu,
|
sdclipgpu,
|
||||||
|
sdclipl,
|
||||||
|
sdconvdirect,
|
||||||
|
sdlora,
|
||||||
|
sdmodel,
|
||||||
|
sdphotomaker,
|
||||||
|
sdt5xxl,
|
||||||
|
sdvae,
|
||||||
|
sdvaecpu,
|
||||||
|
smartcache,
|
||||||
|
tensorSplit,
|
||||||
|
usecuda: acceleration === 'cuda' || acceleration === 'rocm',
|
||||||
|
usemmap,
|
||||||
|
usevulkan: acceleration === 'vulkan',
|
||||||
|
websearch,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateNewConfig = async (configName: string) => {
|
const handleCreateNewConfig = async (configName: string) => {
|
||||||
const fullConfigName = `${configName}.json`;
|
const fullConfigName = `${configName}.json`;
|
||||||
const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
|
const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
|
||||||
fullConfigName,
|
fullConfigName,
|
||||||
buildConfigData()
|
buildConfigData(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (saveSuccess) {
|
if (saveSuccess) {
|
||||||
|
|
@ -210,7 +211,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
|
|
||||||
const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
|
const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
|
||||||
selectedFile,
|
selectedFile,
|
||||||
buildConfigData()
|
buildConfigData(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!saveSuccess) {
|
if (!saveSuccess) {
|
||||||
|
|
@ -260,43 +261,43 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
|
|
||||||
const handleLaunchClick = useCallback(() => {
|
const handleLaunchClick = useCallback(() => {
|
||||||
void handleLaunch({
|
void handleLaunch({
|
||||||
autoGpuLayers,
|
|
||||||
gpuLayers,
|
|
||||||
contextSize,
|
|
||||||
port: port ?? 5001,
|
|
||||||
host,
|
|
||||||
multiuser,
|
|
||||||
multiplayer,
|
|
||||||
remotetunnel,
|
|
||||||
nocertify,
|
|
||||||
websearch,
|
|
||||||
noshift,
|
|
||||||
flashattention,
|
|
||||||
noavx2,
|
|
||||||
failsafe,
|
|
||||||
acceleration,
|
acceleration,
|
||||||
lowvram,
|
|
||||||
gpuDeviceSelection,
|
|
||||||
gpuPlatform,
|
|
||||||
tensorSplit,
|
|
||||||
quantmatmul,
|
|
||||||
usemmap,
|
|
||||||
debugmode,
|
|
||||||
additionalArguments,
|
additionalArguments,
|
||||||
preLaunchCommands,
|
autoGpuLayers,
|
||||||
sdt5xxl,
|
contextSize,
|
||||||
sdclipl,
|
debugmode,
|
||||||
sdclipg,
|
failsafe,
|
||||||
sdphotomaker,
|
flashattention,
|
||||||
sdvae,
|
gpuDeviceSelection,
|
||||||
sdlora,
|
gpuLayers,
|
||||||
sdconvdirect,
|
gpuPlatform,
|
||||||
sdvaecpu,
|
host,
|
||||||
sdclipgpu,
|
lowvram,
|
||||||
moecpu,
|
moecpu,
|
||||||
moeexperts,
|
moeexperts,
|
||||||
smartcache,
|
multiplayer,
|
||||||
|
multiuser,
|
||||||
|
noavx2,
|
||||||
|
nocertify,
|
||||||
|
noshift,
|
||||||
pipelineparallel,
|
pipelineparallel,
|
||||||
|
port: port ?? 5001,
|
||||||
|
preLaunchCommands,
|
||||||
|
quantmatmul,
|
||||||
|
remotetunnel,
|
||||||
|
sdclipg,
|
||||||
|
sdclipgpu,
|
||||||
|
sdclipl,
|
||||||
|
sdconvdirect,
|
||||||
|
sdlora,
|
||||||
|
sdphotomaker,
|
||||||
|
sdt5xxl,
|
||||||
|
sdvae,
|
||||||
|
sdvaecpu,
|
||||||
|
smartcache,
|
||||||
|
tensorSplit,
|
||||||
|
usemmap,
|
||||||
|
websearch,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
handleLaunch,
|
handleLaunch,
|
||||||
|
|
@ -358,17 +359,17 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
|
||||||
maxHeight: '51vh',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
panel: {
|
panel: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
paddingTop: '1rem',
|
paddingTop: '1rem',
|
||||||
paddingRight: '0.5rem',
|
paddingRight: '0.5rem',
|
||||||
},
|
},
|
||||||
|
root: {
|
||||||
|
maxHeight: '51vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
|
|
@ -410,12 +411,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="blue"
|
color="blue"
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '1em',
|
fontSize: '1em',
|
||||||
padding: '0.75rem 1.75rem',
|
fontWeight: 600,
|
||||||
minWidth: '7.5rem',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.03125rem',
|
letterSpacing: '0.03125rem',
|
||||||
|
minWidth: '7.5rem',
|
||||||
|
padding: '0.75rem 1.75rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Launch
|
Launch
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import iconUrl from '/icon.png';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -11,8 +12,8 @@ import {
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import iconUrl from '/icon.png';
|
|
||||||
|
|
||||||
interface WelcomeScreenProps {
|
interface WelcomeScreenProps {
|
||||||
onGetStarted: () => void;
|
onGetStarted: () => void;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
|
import icon from '/icon.png';
|
||||||
import { Badge, Button, Card, Center, Group, Image, rem, Stack, Text } from '@mantine/core';
|
import { Badge, Button, Card, Center, Group, Image, rem, Stack, Text } from '@mantine/core';
|
||||||
import { Github } from 'lucide-react';
|
import { Github } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { GITHUB_API, PRODUCT_NAME } from '@/constants';
|
import { GITHUB_API, PRODUCT_NAME } from '@/constants';
|
||||||
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
||||||
import type { SystemVersionInfo } from '@/types/electron';
|
import type { SystemVersionInfo } from '@/types/electron';
|
||||||
|
|
||||||
import icon from '/icon.png';
|
|
||||||
|
|
||||||
export const AboutTab = () => {
|
export const AboutTab = () => {
|
||||||
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
|
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
|
||||||
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
||||||
|
|
@ -32,6 +32,7 @@ export const AboutTab = () => {
|
||||||
|
|
||||||
const actionButtons = [
|
const actionButtons = [
|
||||||
{
|
{
|
||||||
|
// eslint-disable-next-line typescript/no-deprecated
|
||||||
icon: Github,
|
icon: Github,
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
onClick: () => window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
|
onClick: () => window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
|
||||||
|
|
@ -49,8 +50,8 @@ export const AboutTab = () => {
|
||||||
h={64}
|
h={64}
|
||||||
onClick={() => void handleLogoClick()}
|
onClick={() => void handleLogoClick()}
|
||||||
style={{
|
style={{
|
||||||
minWidth: 64,
|
|
||||||
minHeight: 64,
|
minHeight: 64,
|
||||||
|
minWidth: 64,
|
||||||
...getLogoStyles(),
|
...getLogoStyles(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -72,7 +73,7 @@ export const AboutTab = () => {
|
||||||
key={button.label}
|
key={button.label}
|
||||||
variant="light"
|
variant="light"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={<button.icon style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<button.icon style={{ height: rem(16), width: rem(16) }} />}
|
||||||
onClick={() => void button.onClick()}
|
onClick={() => void button.onClick()}
|
||||||
style={button.label === 'GitHub' ? { textDecoration: 'none' } : undefined}
|
style={button.label === 'GitHub' ? { textDecoration: 'none' } : undefined}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
import {
|
import { Group, rem, SegmentedControl, Slider, Stack, Text, TextInput } from '@mantine/core';
|
||||||
Group,
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
type MantineColorScheme,
|
|
||||||
rem,
|
|
||||||
SegmentedControl,
|
|
||||||
Slider,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
||||||
import { ZOOM } from '@/constants';
|
import { ZOOM } from '@/constants';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
@ -82,19 +75,19 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
|
||||||
value={rawColorScheme}
|
value={rawColorScheme}
|
||||||
onChange={handleColorSchemeChange}
|
onChange={handleColorSchemeChange}
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
root: {
|
|
||||||
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
|
||||||
},
|
|
||||||
indicator: {
|
indicator: {
|
||||||
backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
|
backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
|
||||||
border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
||||||
},
|
},
|
||||||
|
root: {
|
||||||
|
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Group gap="xs" justify="center">
|
<Group gap="xs" justify="center">
|
||||||
<Sun style={{ width: rem(16), height: rem(16) }} />
|
<Sun style={{ height: rem(16), width: rem(16) }} />
|
||||||
<span>Light</span>
|
<span>Light</span>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
|
@ -103,7 +96,7 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Group gap="xs" justify="center">
|
<Group gap="xs" justify="center">
|
||||||
<Moon style={{ width: rem(16), height: rem(16) }} />
|
<Moon style={{ height: rem(16), width: rem(16) }} />
|
||||||
<span>Dark</span>
|
<span>Dark</span>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
|
@ -112,7 +105,7 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
<Group gap="xs" justify="center">
|
<Group gap="xs" justify="center">
|
||||||
<Monitor style={{ width: rem(16), height: rem(16) }} />
|
<Monitor style={{ height: rem(16), width: rem(16) }} />
|
||||||
<span>System</span>
|
<span>System</span>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
|
@ -133,7 +126,7 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
|
||||||
value={zoomPercentage}
|
value={zoomPercentage}
|
||||||
onChange={(event) => handleZoomPercentageChange(event.currentTarget.value)}
|
onChange={(event) => handleZoomPercentageChange(event.currentTarget.value)}
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
const value = event.currentTarget.value;
|
const { value } = event.currentTarget;
|
||||||
const numValue = Number(value);
|
const numValue = Number(value);
|
||||||
if (Number.isNaN(numValue) || !isValidZoomPercentage(numValue)) {
|
if (Number.isNaN(numValue) || !isValidZoomPercentage(numValue)) {
|
||||||
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());
|
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core';
|
import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core';
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
import { ImportBackendLink } from '@/components/ImportBackendLink';
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
|
|
@ -75,35 +76,35 @@ export const BackendsTab = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(
|
||||||
installedBackend && currentBackend && currentBackend.path === installedBackend.path
|
installedBackend && currentBackend && currentBackend.path === installedBackend.path,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (installedBackend) {
|
if (installedBackend) {
|
||||||
processedInstalled.add(installedBackend.path);
|
processedInstalled.add(installedBackend.path);
|
||||||
|
|
||||||
const hasUpdate =
|
const hasUpdate =
|
||||||
compareVersions(download.version || 'unknown', installedBackend.version) > 0;
|
compareVersions(download.version ?? 'unknown', installedBackend.version) > 0;
|
||||||
|
|
||||||
backends.push({
|
backends.push({
|
||||||
name: download.name,
|
|
||||||
version: installedBackend.version,
|
|
||||||
size: undefined,
|
|
||||||
isInstalled: true,
|
|
||||||
isCurrent,
|
|
||||||
downloadUrl: download.url,
|
|
||||||
installedPath: installedBackend.path,
|
|
||||||
hasUpdate,
|
|
||||||
newerVersion: hasUpdate ? download.version : undefined,
|
|
||||||
actualVersion: installedBackend.actualVersion,
|
actualVersion: installedBackend.actualVersion,
|
||||||
|
downloadUrl: download.url,
|
||||||
|
hasUpdate,
|
||||||
|
installedPath: installedBackend.path,
|
||||||
|
isCurrent,
|
||||||
|
isInstalled: true,
|
||||||
|
name: download.name,
|
||||||
|
newerVersion: hasUpdate ? download.version : undefined,
|
||||||
|
size: undefined,
|
||||||
|
version: installedBackend.version,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
backends.push({
|
backends.push({
|
||||||
name: download.name,
|
|
||||||
version: download.version || 'unknown',
|
|
||||||
size: download.size,
|
|
||||||
isInstalled: false,
|
|
||||||
isCurrent: false,
|
|
||||||
downloadUrl: download.url,
|
downloadUrl: download.url,
|
||||||
|
isCurrent: false,
|
||||||
|
isInstalled: false,
|
||||||
|
name: download.name,
|
||||||
|
size: download.size,
|
||||||
|
version: download.version ?? 'unknown',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -114,20 +115,24 @@ export const BackendsTab = () => {
|
||||||
const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path);
|
const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path);
|
||||||
|
|
||||||
backends.push({
|
backends.push({
|
||||||
name: displayName,
|
|
||||||
version: installed.version,
|
|
||||||
size: undefined,
|
|
||||||
isInstalled: true,
|
|
||||||
isCurrent,
|
|
||||||
installedPath: installed.path,
|
|
||||||
actualVersion: installed.actualVersion,
|
actualVersion: installed.actualVersion,
|
||||||
|
installedPath: installed.path,
|
||||||
|
isCurrent,
|
||||||
|
isInstalled: true,
|
||||||
|
name: displayName,
|
||||||
|
size: undefined,
|
||||||
|
version: installed.version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return backends.sort((a, b) => {
|
return backends.toSorted((a, b) => {
|
||||||
if (a.isInstalled && !b.isInstalled) return -1;
|
if (a.isInstalled && !b.isInstalled) {
|
||||||
if (!a.isInstalled && b.isInstalled) return 1;
|
return -1;
|
||||||
|
}
|
||||||
|
if (!a.isInstalled && b.isInstalled) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
@ -144,11 +149,13 @@ export const BackendsTab = () => {
|
||||||
|
|
||||||
const handleDownload = async (backend: BackendInfo) => {
|
const handleDownload = async (backend: BackendInfo) => {
|
||||||
const download = availableDownloads.find((d) => d.name === backend.name);
|
const download = availableDownloads.find((d) => d.name === backend.name);
|
||||||
if (!download) return;
|
if (!download) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await handleDownloadFromStore({
|
await handleDownloadFromStore({
|
||||||
item: download,
|
|
||||||
isUpdate: false,
|
isUpdate: false,
|
||||||
|
item: download,
|
||||||
wasCurrentBinary: false,
|
wasCurrentBinary: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -157,13 +164,15 @@ export const BackendsTab = () => {
|
||||||
|
|
||||||
const handleUpdate = async (backend: BackendInfo) => {
|
const handleUpdate = async (backend: BackendInfo) => {
|
||||||
const download = availableDownloads.find((d) => d.name === backend.name);
|
const download = availableDownloads.find((d) => d.name === backend.name);
|
||||||
if (!download) return;
|
if (!download) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await handleDownloadFromStore({
|
await handleDownloadFromStore({
|
||||||
item: download,
|
|
||||||
isUpdate: true,
|
isUpdate: true,
|
||||||
wasCurrentBinary: backend.isCurrent,
|
item: download,
|
||||||
oldBackendPath: backend.installedPath,
|
oldBackendPath: backend.installedPath,
|
||||||
|
wasCurrentBinary: backend.isCurrent,
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadInstalledBackends();
|
await loadInstalledBackends();
|
||||||
|
|
@ -171,20 +180,24 @@ export const BackendsTab = () => {
|
||||||
|
|
||||||
const handleRedownload = async (backend: BackendInfo) => {
|
const handleRedownload = async (backend: BackendInfo) => {
|
||||||
const download = availableDownloads.find((d) => d.name === backend.name);
|
const download = availableDownloads.find((d) => d.name === backend.name);
|
||||||
if (!download) return;
|
if (!download) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await handleDownloadFromStore({
|
await handleDownloadFromStore({
|
||||||
item: download,
|
|
||||||
isUpdate: true,
|
isUpdate: true,
|
||||||
wasCurrentBinary: backend.isCurrent,
|
item: download,
|
||||||
oldBackendPath: backend.installedPath,
|
oldBackendPath: backend.installedPath,
|
||||||
|
wasCurrentBinary: backend.isCurrent,
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadInstalledBackends();
|
await loadInstalledBackends();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (backend: BackendInfo) => {
|
const handleDelete = async (backend: BackendInfo) => {
|
||||||
if (!backend.installedPath || backend.isCurrent) return;
|
if (!backend.installedPath || backend.isCurrent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath);
|
const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -193,7 +206,9 @@ export const BackendsTab = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeCurrent = (backend: BackendInfo) => {
|
const makeCurrent = (backend: BackendInfo) => {
|
||||||
if (!backend.installedPath) return;
|
if (!backend.installedPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const targetBackend = installedBackends.find((b) => b.path === backend.installedPath);
|
const targetBackend = installedBackends.find((b) => b.path === backend.installedPath);
|
||||||
if (targetBackend) {
|
if (targetBackend) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Anchor, Box, Button, Group, rem, Stack, Text } from '@mantine/core';
|
import { Anchor, Box, Button, Group, Stack, Text, rem } from '@mantine/core';
|
||||||
import { Image, Monitor } from 'lucide-react';
|
import { Image, Monitor } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
import { FRONTENDS } from '@/constants';
|
import { FRONTENDS } from '@/constants';
|
||||||
|
|
@ -41,16 +42,16 @@ export const FrontendInterfaceSelector = ({
|
||||||
const frontendConfigs: FrontendConfig[] = useMemo(
|
const frontendConfigs: FrontendConfig[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
value: 'llamacpp',
|
|
||||||
label: FRONTENDS.LLAMA_CPP,
|
label: FRONTENDS.LLAMA_CPP,
|
||||||
|
value: 'llamacpp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'koboldcpp',
|
|
||||||
label: FRONTENDS.KOBOLDAI_LITE,
|
label: FRONTENDS.KOBOLDAI_LITE,
|
||||||
|
value: 'koboldcpp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'sillytavern',
|
|
||||||
label: FRONTENDS.SILLYTAVERN,
|
label: FRONTENDS.SILLYTAVERN,
|
||||||
|
requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(),
|
||||||
requirements: [
|
requirements: [
|
||||||
{
|
{
|
||||||
id: 'nodejs',
|
id: 'nodejs',
|
||||||
|
|
@ -58,11 +59,11 @@ export const FrontendInterfaceSelector = ({
|
||||||
url: 'https://nodejs.org/',
|
url: 'https://nodejs.org/',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(),
|
value: 'sillytavern',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'openwebui',
|
|
||||||
label: FRONTENDS.OPENWEBUI,
|
label: FRONTENDS.OPENWEBUI,
|
||||||
|
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
|
||||||
requirements: [
|
requirements: [
|
||||||
{
|
{
|
||||||
id: 'uv',
|
id: 'uv',
|
||||||
|
|
@ -70,10 +71,10 @@ export const FrontendInterfaceSelector = ({
|
||||||
url: 'https://docs.astral.sh/uv/getting-started/installation/',
|
url: 'https://docs.astral.sh/uv/getting-started/installation/',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
|
value: 'openwebui',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkAllFrontendRequirements = useCallback(async () => {
|
const checkAllFrontendRequirements = useCallback(async () => {
|
||||||
|
|
@ -91,7 +92,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
setFrontendRequirements(requirementResults);
|
setFrontendRequirements(requirementResults);
|
||||||
|
|
||||||
const currentFrontendConfig = frontendConfigs.find(
|
const currentFrontendConfig = frontendConfigs.find(
|
||||||
(config) => config.value === frontendPreference
|
(config) => config.value === frontendPreference,
|
||||||
);
|
);
|
||||||
if (currentFrontendConfig && !requirementResults.get(frontendPreference)) {
|
if (currentFrontendConfig && !requirementResults.get(frontendPreference)) {
|
||||||
setFrontendPreference('llamacpp');
|
setFrontendPreference('llamacpp');
|
||||||
|
|
@ -103,7 +104,9 @@ export const FrontendInterfaceSelector = ({
|
||||||
|
|
||||||
const getUnmetRequirements = () => {
|
const getUnmetRequirements = () => {
|
||||||
const selectedConfig = getSelectedFrontendConfig();
|
const selectedConfig = getSelectedFrontendConfig();
|
||||||
if (!selectedConfig || !selectedConfig.requirements) return [];
|
if (!selectedConfig || !selectedConfig.requirements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true;
|
const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true;
|
||||||
return isAvailable ? [] : selectedConfig.requirements;
|
return isAvailable ? [] : selectedConfig.requirements;
|
||||||
|
|
@ -111,7 +114,9 @@ export const FrontendInterfaceSelector = ({
|
||||||
|
|
||||||
const getUnmetRequirementsForFrontend = (frontendValue: string) => {
|
const getUnmetRequirementsForFrontend = (frontendValue: string) => {
|
||||||
const config = frontendConfigs.find((c) => c.value === frontendValue);
|
const config = frontendConfigs.find((c) => c.value === frontendValue);
|
||||||
if (!config || !config.requirements) return [];
|
if (!config || !config.requirements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const isAvailable = frontendRequirements.get(frontendValue) ?? true;
|
const isAvailable = frontendRequirements.get(frontendValue) ?? true;
|
||||||
return isAvailable ? [] : config.requirements;
|
return isAvailable ? [] : config.requirements;
|
||||||
|
|
@ -135,7 +140,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
|
|
||||||
const renderDisabledFrontendWarnings = () => {
|
const renderDisabledFrontendWarnings = () => {
|
||||||
const disabledFrontends = frontendConfigs.filter(
|
const disabledFrontends = frontendConfigs.filter(
|
||||||
(config) => !isFrontendAvailable(config.value)
|
(config) => !isFrontendAvailable(config.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (disabledFrontends.length === 0) {
|
if (disabledFrontends.length === 0) {
|
||||||
|
|
@ -155,12 +160,12 @@ export const FrontendInterfaceSelector = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mt="sm">
|
<Box mt="sm">
|
||||||
{Array.from(requirementGroups.entries()).map(([reqKey, frontendLabels]) => {
|
{[...requirementGroups.entries()].map(([reqKey, frontendLabels]) => {
|
||||||
const firstDisabledFrontend = disabledFrontends.find(
|
const firstDisabledFrontend = disabledFrontends.find(
|
||||||
(config) =>
|
(config) =>
|
||||||
getUnmetRequirementsForFrontend(config.value)
|
getUnmetRequirementsForFrontend(config.value)
|
||||||
.map((req) => req.id)
|
.map((req) => req.id)
|
||||||
.join(',') === reqKey
|
.join(',') === reqKey,
|
||||||
);
|
);
|
||||||
const unmetReqs = firstDisabledFrontend
|
const unmetReqs = firstDisabledFrontend
|
||||||
? getUnmetRequirementsForFrontend(firstDisabledFrontend.value)
|
? getUnmetRequirementsForFrontend(firstDisabledFrontend.value)
|
||||||
|
|
@ -253,11 +258,11 @@ export const FrontendInterfaceSelector = ({
|
||||||
onChange={handleFrontendPreferenceChange}
|
onChange={handleFrontendPreferenceChange}
|
||||||
disabled={isOnInterfaceScreen}
|
disabled={isOnInterfaceScreen}
|
||||||
data={frontendConfigs.map((config) => ({
|
data={frontendConfigs.map((config) => ({
|
||||||
value: config.value,
|
|
||||||
label: config.label,
|
|
||||||
disabled: !isFrontendAvailable(config.value),
|
disabled: !isFrontendAvailable(config.value),
|
||||||
|
label: config.label,
|
||||||
|
value: config.value,
|
||||||
}))}
|
}))}
|
||||||
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Monitor style={{ height: rem(16), width: rem(16) }} />}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -322,10 +327,10 @@ export const FrontendInterfaceSelector = ({
|
||||||
onChange={handleImageGenerationFrontendChange}
|
onChange={handleImageGenerationFrontendChange}
|
||||||
disabled={isOnInterfaceScreen}
|
disabled={isOnInterfaceScreen}
|
||||||
data={[
|
data={[
|
||||||
{ value: 'match', label: 'Match Frontend' },
|
{ label: 'Match Frontend', value: 'match' },
|
||||||
{ value: 'builtin', label: 'Built-in' },
|
{ label: 'Built-in', value: 'builtin' },
|
||||||
]}
|
]}
|
||||||
leftSection={<Image style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Image style={{ height: rem(16), width: rem(16) }} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Stack, Text } from '@mantine/core';
|
import { Stack, Text } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Switch } from '@/components/Switch';
|
import { Switch } from '@/components/Switch';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Group, rem, Tabs, Text } from '@mantine/core';
|
import { Group, Tabs, Text, rem } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Info,
|
Info,
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Wrench,
|
Wrench,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { AboutTab } from '@/components/settings/AboutTab';
|
import { AboutTab } from '@/components/settings/AboutTab';
|
||||||
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
||||||
|
|
@ -72,16 +73,16 @@ export const SettingsModal = ({
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
variant="pills"
|
variant="pills"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
},
|
|
||||||
panel: {
|
panel: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
paddingLeft: '1.5rem',
|
paddingLeft: '1.5rem',
|
||||||
paddingRight: '1.5rem',
|
paddingRight: '1.5rem',
|
||||||
},
|
},
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
},
|
||||||
tabLabel: {
|
tabLabel: {
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
|
|
@ -91,39 +92,39 @@ export const SettingsModal = ({
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="general"
|
value="general"
|
||||||
leftSection={<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<SlidersHorizontal style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
General
|
General
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
{showBackendsTab && (
|
{showBackendsTab && (
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="backends"
|
value="backends"
|
||||||
leftSection={<GitBranch style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<GitBranch style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
Backends
|
Backends
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
)}
|
)}
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="appearance"
|
value="appearance"
|
||||||
leftSection={<Palette style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Palette style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
Appearance
|
Appearance
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="system"
|
value="system"
|
||||||
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Monitor style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
System
|
System
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="troubleshooting"
|
value="troubleshooting"
|
||||||
leftSection={<Wrench style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Wrench style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
Troubleshooting
|
Troubleshooting
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="about"
|
value="about"
|
||||||
leftSection={<Info style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Info style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
About
|
About
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Center, Stack, Text } from '@mantine/core';
|
import { Center, Stack, Text } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { InfoCard } from '@/components/InfoCard';
|
import { InfoCard } from '@/components/InfoCard';
|
||||||
import type { SystemVersionInfo } from '@/types/electron';
|
import type { SystemVersionInfo } from '@/types/electron';
|
||||||
import type { HardwareInfo } from '@/types/hardware';
|
import type { HardwareInfo } from '@/types/hardware';
|
||||||
|
|
@ -22,7 +23,7 @@ export const SystemTab = () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setVersionInfo(info);
|
setVersionInfo(info);
|
||||||
setKoboldVersion(currentBackend?.version || null);
|
setKoboldVersion(currentBackend?.version ?? null);
|
||||||
|
|
||||||
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
|
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
|
||||||
window.electronAPI.kobold.detectCPU(),
|
window.electronAPI.kobold.detectCPU(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Group, rem, Stack, Text, TextInput } from '@mantine/core';
|
import { Button, Group, Stack, Text, TextInput, rem } from '@mantine/core';
|
||||||
import { ExternalLink, Folder, FolderOpen, Monitor } from 'lucide-react';
|
import { ExternalLink, Folder, FolderOpen, Monitor } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
|
@ -44,12 +44,12 @@ export const TroubleshootingTab = () => {
|
||||||
readOnly
|
readOnly
|
||||||
placeholder="Default installation directory"
|
placeholder="Default installation directory"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Folder style={{ height: rem(16), width: rem(16) }} />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => void handleSelectInstallDir()}
|
onClick={() => void handleSelectInstallDir()}
|
||||||
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<FolderOpen style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
Browse
|
Browse
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -57,7 +57,7 @@ export const TroubleshootingTab = () => {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => void handleOpenInstallDir()}
|
onClick={() => void handleOpenInstallDir()}
|
||||||
disabled={!installDir}
|
disabled={!installDir}
|
||||||
leftSection={<ExternalLink style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<ExternalLink style={{ height: rem(16), width: rem(16) }} />}
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -75,7 +75,7 @@ export const TroubleshootingTab = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<FolderOpen style={{ height: rem(16), width: rem(16) }} />}
|
||||||
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
||||||
>
|
>
|
||||||
Show Logs
|
Show Logs
|
||||||
|
|
@ -83,7 +83,7 @@ export const TroubleshootingTab = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Monitor style={{ height: rem(16), width: rem(16) }} />}
|
||||||
onClick={() => void window.electronAPI.app.viewConfigFile()}
|
onClick={() => void window.electronAPI.app.viewConfigFile()}
|
||||||
>
|
>
|
||||||
View Config
|
View Config
|
||||||
|
|
|
||||||
|
|
@ -13,38 +13,38 @@ export interface ImageModelPreset {
|
||||||
export const IMAGE_MODEL_PRESETS: readonly ImageModelPreset[] = [
|
export const IMAGE_MODEL_PRESETS: readonly ImageModelPreset[] = [
|
||||||
{
|
{
|
||||||
name: 'Z-Image',
|
name: 'Z-Image',
|
||||||
sdmodel: `${HUGGINGFACE_BASE_URL}/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_0.gguf`,
|
|
||||||
sdt5xxl: '',
|
|
||||||
sdclipl: `${HUGGINGFACE_BASE_URL}/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_S.gguf`,
|
|
||||||
sdclipg: '',
|
sdclipg: '',
|
||||||
|
sdclipl: `${HUGGINGFACE_BASE_URL}/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_S.gguf`,
|
||||||
|
sdmodel: `${HUGGINGFACE_BASE_URL}/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_0.gguf`,
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
|
sdt5xxl: '',
|
||||||
sdvae: `${HUGGINGFACE_BASE_URL}/koboldcpp/GGUFDumps/resolve/main/flux1vae.safetensors`,
|
sdvae: `${HUGGINGFACE_BASE_URL}/koboldcpp/GGUFDumps/resolve/main/flux1vae.safetensors`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'FLUX.1',
|
name: 'FLUX.1',
|
||||||
sdmodel: `${HUGGINGFACE_BASE_URL}/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf`,
|
|
||||||
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
|
|
||||||
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
|
|
||||||
sdclipg: '',
|
sdclipg: '',
|
||||||
|
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
|
||||||
|
sdmodel: `${HUGGINGFACE_BASE_URL}/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf`,
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
|
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
|
||||||
sdvae: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/ae.safetensors`,
|
sdvae: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/ae.safetensors`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chroma',
|
name: 'Chroma',
|
||||||
sdmodel: `${HUGGINGFACE_BASE_URL}/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v45/chroma-unlocked-v45-Q4_0.gguf`,
|
|
||||||
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
|
|
||||||
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
|
|
||||||
sdclipg: '',
|
sdclipg: '',
|
||||||
|
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
|
||||||
|
sdmodel: `${HUGGINGFACE_BASE_URL}/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v45/chroma-unlocked-v45-Q4_0.gguf`,
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
|
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
|
||||||
sdvae: `${HUGGINGFACE_BASE_URL}/lodestones/Chroma/resolve/main/ae.safetensors`,
|
sdvae: `${HUGGINGFACE_BASE_URL}/lodestones/Chroma/resolve/main/ae.safetensors`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Qwen Image Edit 2509',
|
name: 'Qwen Image Edit 2509',
|
||||||
sdmodel: `${HUGGINGFACE_BASE_URL}/QuantStack/Qwen-Image-Edit-2509-GGUF/resolve/main/Qwen-Image-Edit-2509-Q4_K_S.gguf`,
|
|
||||||
sdt5xxl: '',
|
|
||||||
sdclipl: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf`,
|
|
||||||
sdclipg: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf`,
|
sdclipg: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf`,
|
||||||
|
sdclipl: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf`,
|
||||||
|
sdmodel: `${HUGGINGFACE_BASE_URL}/QuantStack/Qwen-Image-Edit-2509-GGUF/resolve/main/Qwen-Image-Edit-2509-Q4_K_S.gguf`,
|
||||||
sdphotomaker: '',
|
sdphotomaker: '',
|
||||||
|
sdt5xxl: '',
|
||||||
sdvae: `${HUGGINGFACE_BASE_URL}/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors`,
|
sdvae: `${HUGGINGFACE_BASE_URL}/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors`,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ export const HUGGINGFACE_BASE_URL = 'https://huggingface.co';
|
||||||
|
|
||||||
export const SERVER_READY_SIGNALS = {
|
export const SERVER_READY_SIGNALS = {
|
||||||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||||
SILLYTAVERN: 'SillyTavern is listening on',
|
|
||||||
OPENWEBUI: 'Started server process',
|
OPENWEBUI: 'Started server process',
|
||||||
|
SILLYTAVERN: 'SillyTavern is listening on',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_CONTEXT_SIZE = 4096;
|
export const DEFAULT_CONTEXT_SIZE = 4096;
|
||||||
|
|
@ -24,12 +24,12 @@ export const SILLYTAVERN = {
|
||||||
HOST: 'localhost',
|
HOST: 'localhost',
|
||||||
PORT: 3000,
|
PORT: 3000,
|
||||||
PROXY_PORT: 3001,
|
PROXY_PORT: 3001,
|
||||||
get URL() {
|
|
||||||
return `http://${this.HOST}:${this.PORT}` as const;
|
|
||||||
},
|
|
||||||
get PROXY_URL() {
|
get PROXY_URL() {
|
||||||
return `http://${this.HOST}:${this.PROXY_PORT}` as const;
|
return `http://${this.HOST}:${this.PROXY_PORT}` as const;
|
||||||
},
|
},
|
||||||
|
get URL() {
|
||||||
|
return `http://${this.HOST}:${this.PORT}` as const;
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const OPENWEBUI = {
|
export const OPENWEBUI = {
|
||||||
|
|
@ -41,29 +41,29 @@ export const OPENWEBUI = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const GITHUB_API = {
|
export const GITHUB_API = {
|
||||||
BASE_URL: 'https://api.github.com',
|
|
||||||
GITHUB_BASE_URL: 'https://github.com',
|
|
||||||
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
|
||||||
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
|
||||||
GERBIL_REPO: 'lone-cloud/gerbil',
|
|
||||||
CLOUDFLARED_REPO: 'cloudflare/cloudflared',
|
|
||||||
get LATEST_RELEASE_URL() {
|
|
||||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest` as const;
|
|
||||||
},
|
|
||||||
get ALL_RELEASES_URL() {
|
get ALL_RELEASES_URL() {
|
||||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases` as const;
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases` as const;
|
||||||
},
|
},
|
||||||
get ROCM_LATEST_RELEASE_URL() {
|
BASE_URL: 'https://api.github.com',
|
||||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest` as const;
|
get CLOUDFLARED_LATEST_RELEASE_URL() {
|
||||||
|
return `${this.BASE_URL}/repos/${this.CLOUDFLARED_REPO}/releases/latest` as const;
|
||||||
},
|
},
|
||||||
|
CLOUDFLARED_REPO: 'cloudflare/cloudflared',
|
||||||
get GERBIL_GITHUB_URL() {
|
get GERBIL_GITHUB_URL() {
|
||||||
return `${this.GITHUB_BASE_URL}/${this.GERBIL_REPO}` as const;
|
return `${this.GITHUB_BASE_URL}/${this.GERBIL_REPO}` as const;
|
||||||
},
|
},
|
||||||
get GERBIL_LATEST_RELEASE_URL() {
|
get GERBIL_LATEST_RELEASE_URL() {
|
||||||
return `${this.BASE_URL}/repos/${this.GERBIL_REPO}/releases/latest` as const;
|
return `${this.BASE_URL}/repos/${this.GERBIL_REPO}/releases/latest` as const;
|
||||||
},
|
},
|
||||||
get CLOUDFLARED_LATEST_RELEASE_URL() {
|
GERBIL_REPO: 'lone-cloud/gerbil',
|
||||||
return `${this.BASE_URL}/repos/${this.CLOUDFLARED_REPO}/releases/latest` as const;
|
GITHUB_BASE_URL: 'https://github.com',
|
||||||
|
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||||
|
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
||||||
|
get LATEST_RELEASE_URL() {
|
||||||
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest` as const;
|
||||||
|
},
|
||||||
|
get ROCM_LATEST_RELEASE_URL() {
|
||||||
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest` as const;
|
||||||
},
|
},
|
||||||
getCloudflaredDownloadUrl(version: string, filename: string) {
|
getCloudflaredDownloadUrl(version: string, filename: string) {
|
||||||
return `${this.GITHUB_BASE_URL}/${this.CLOUDFLARED_REPO}/releases/download/${version}/${filename}` as const;
|
return `${this.GITHUB_BASE_URL}/${this.CLOUDFLARED_REPO}/releases/download/${version}/${filename}` as const;
|
||||||
|
|
@ -71,9 +71,9 @@ export const GITHUB_API = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ASSET_SUFFIXES = {
|
export const ASSET_SUFFIXES = {
|
||||||
ROCM: 'rocm',
|
|
||||||
NOCUDA: 'nocuda',
|
NOCUDA: 'nocuda',
|
||||||
OLDPC: 'oldpc',
|
OLDPC: 'oldpc',
|
||||||
|
ROCM: 'rocm',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ROCM = {
|
export const ROCM = {
|
||||||
|
|
@ -87,16 +87,16 @@ export const ROCM = {
|
||||||
export const FRONTENDS = {
|
export const FRONTENDS = {
|
||||||
KOBOLDAI_LITE: 'KoboldAI Lite',
|
KOBOLDAI_LITE: 'KoboldAI Lite',
|
||||||
LLAMA_CPP: 'llama.cpp',
|
LLAMA_CPP: 'llama.cpp',
|
||||||
STABLE_UI: 'Stable UI',
|
|
||||||
SILLYTAVERN: 'SillyTavern',
|
|
||||||
OPENWEBUI: 'Open WebUI',
|
OPENWEBUI: 'Open WebUI',
|
||||||
|
SILLYTAVERN: 'SillyTavern',
|
||||||
|
STABLE_UI: 'Stable UI',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ZOOM = {
|
export const ZOOM = {
|
||||||
MIN_LEVEL: -3,
|
|
||||||
MAX_LEVEL: 3,
|
|
||||||
MIN_PERCENTAGE: 25,
|
|
||||||
MAX_PERCENTAGE: 300,
|
|
||||||
DEFAULT_LEVEL: 0,
|
DEFAULT_LEVEL: 0,
|
||||||
DEFAULT_PERCENTAGE: 100,
|
DEFAULT_PERCENTAGE: 100,
|
||||||
|
MAX_LEVEL: 3,
|
||||||
|
MAX_PERCENTAGE: 300,
|
||||||
|
MIN_LEVEL: -3,
|
||||||
|
MIN_PERCENTAGE: 25,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export const DEFAULT_NOTEPAD_POSITION = {
|
export const DEFAULT_NOTEPAD_POSITION = {
|
||||||
width: 400,
|
|
||||||
height: 400,
|
height: 400,
|
||||||
|
width: 400,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NOTEPAD_MIN_WIDTH = 300;
|
export const NOTEPAD_MIN_WIDTH = 300;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { GITHUB_API } from '@/constants';
|
import { GITHUB_API } from '@/constants';
|
||||||
import { compareVersions } from '@/utils/version';
|
import { compareVersions } from '@/utils/version';
|
||||||
|
|
||||||
|
|
@ -30,7 +31,9 @@ export const useAppUpdateChecker = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const downloadUpdate = useCallback(async () => {
|
const downloadUpdate = useCallback(async () => {
|
||||||
if (!canAutoUpdate) return;
|
if (!canAutoUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
|
|
||||||
|
|
@ -59,7 +62,7 @@ export const useAppUpdateChecker = () => {
|
||||||
const currentVersion = await window.electronAPI.app.getVersion();
|
const currentVersion = await window.electronAPI.app.getVersion();
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${GITHUB_API.BASE_URL}/repos/${GITHUB_API.GERBIL_REPO}/releases/latest`
|
`${GITHUB_API.BASE_URL}/repos/${GITHUB_API.GERBIL_REPO}/releases/latest`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -67,7 +70,7 @@ export const useAppUpdateChecker = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const release = await response.json();
|
const release = await response.json();
|
||||||
const latestVersion = release.tag_name?.replace(/^v/, '') || '';
|
const latestVersion = release.tag_name?.replace(/^v/, '') ?? '';
|
||||||
|
|
||||||
if (!latestVersion) {
|
if (!latestVersion) {
|
||||||
throw new Error('Invalid release data');
|
throw new Error('Invalid release data');
|
||||||
|
|
@ -75,18 +78,18 @@ export const useAppUpdateChecker = () => {
|
||||||
|
|
||||||
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
||||||
|
|
||||||
const updateInfo: AppUpdateInfo = {
|
const info: AppUpdateInfo = {
|
||||||
currentVersion,
|
currentVersion,
|
||||||
|
hasUpdate,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
releaseUrl: release.html_url,
|
releaseUrl: release.html_url,
|
||||||
hasUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setUpdateInfo(updateInfo);
|
setUpdateInfo(info);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electronAPI.logs.logError(
|
window.electronAPI.logs.logError(
|
||||||
'Failed to check for app updates',
|
'Failed to check for app updates',
|
||||||
error instanceof Error ? error : undefined
|
error instanceof Error ? error : undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -96,12 +99,12 @@ export const useAppUpdateChecker = () => {
|
||||||
}, [checkForAppUpdates]);
|
}, [checkForAppUpdates]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
releaseUrl: updateInfo?.releaseUrl,
|
|
||||||
hasUpdate: updateInfo?.hasUpdate || false,
|
|
||||||
canAutoUpdate,
|
canAutoUpdate,
|
||||||
isUpdateDownloaded,
|
|
||||||
isDownloading,
|
|
||||||
downloadUpdate,
|
downloadUpdate,
|
||||||
|
hasUpdate: updateInfo?.hasUpdate ?? false,
|
||||||
installUpdate,
|
installUpdate,
|
||||||
|
isDownloading,
|
||||||
|
isUpdateDownloaded,
|
||||||
|
releaseUrl: updateInfo?.releaseUrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
HuggingFaceFileInfo,
|
HuggingFaceFileInfo,
|
||||||
|
|
@ -49,7 +50,7 @@ const extractParamSize = (name: string) => {
|
||||||
|
|
||||||
const extractBaseModelId = (tags: string[]) => {
|
const extractBaseModelId = (tags: string[]) => {
|
||||||
const baseModelTag = tags.find(
|
const baseModelTag = tags.find(
|
||||||
(tag) => tag.startsWith('base_model:') && !tag.includes('quantized')
|
(tag) => tag.startsWith('base_model:') && !tag.includes('quantized'),
|
||||||
);
|
);
|
||||||
return baseModelTag?.replace('base_model:', '');
|
return baseModelTag?.replace('base_model:', '');
|
||||||
};
|
};
|
||||||
|
|
@ -69,7 +70,9 @@ const formatParamCount = (paramCount: number) => {
|
||||||
const fetchBaseModelParams = async (baseModelId: string) => {
|
const fetchBaseModelParams = async (baseModelId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${HF_API_BASE}/models/${baseModelId}`);
|
const response = await fetch(`${HF_API_BASE}/models/${baseModelId}`);
|
||||||
if (!response.ok) return undefined;
|
if (!response.ok) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const data: HFApiBaseModel = await response.json();
|
const data: HFApiBaseModel = await response.json();
|
||||||
return data.safetensors?.total;
|
return data.safetensors?.total;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -109,9 +112,15 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
const searchQuery = query !== undefined ? query.trim() || undefined : searchParams.search;
|
const searchQuery = query !== undefined ? query.trim() || undefined : searchParams.search;
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (searchQuery) params.set('search', searchQuery);
|
if (searchQuery) {
|
||||||
if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag);
|
params.set('search', searchQuery);
|
||||||
if (searchParams.filter) params.set('filter', searchParams.filter);
|
}
|
||||||
|
if (searchParams.pipelineTag) {
|
||||||
|
params.set('pipeline_tag', searchParams.pipelineTag);
|
||||||
|
}
|
||||||
|
if (searchParams.filter) {
|
||||||
|
params.set('filter', searchParams.filter);
|
||||||
|
}
|
||||||
params.set('sort', sort);
|
params.set('sort', sort);
|
||||||
params.set('limit', String(MODELS_PER_PAGE));
|
params.set('limit', String(MODELS_PER_PAGE));
|
||||||
params.set('full', 'false');
|
params.set('full', 'false');
|
||||||
|
|
@ -142,16 +151,16 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: model.id,
|
|
||||||
name,
|
|
||||||
author,
|
author,
|
||||||
downloads: model.downloads ?? 0,
|
downloads: model.downloads ?? 0,
|
||||||
likes: model.likes ?? 0,
|
|
||||||
updatedAt: new Date(model.lastModified),
|
|
||||||
gated: model.gated ?? false,
|
gated: model.gated ?? false,
|
||||||
|
id: model.id,
|
||||||
|
likes: model.likes ?? 0,
|
||||||
|
name,
|
||||||
paramSize,
|
paramSize,
|
||||||
|
updatedAt: new Date(model.lastModified),
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
setModels(results);
|
setModels(results);
|
||||||
|
|
@ -165,18 +174,20 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[searchParams, sortBy]
|
[searchParams, sortBy],
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeSortOrder = useCallback(
|
const changeSortOrder = useCallback(
|
||||||
(sort: HuggingFaceSortOption, query?: string) => {
|
(sort: HuggingFaceSortOption, query?: string) => {
|
||||||
void searchModels(query, true, sort);
|
void searchModels(query, true, sort);
|
||||||
},
|
},
|
||||||
[searchModels]
|
[searchModels],
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadMoreModels = useCallback(async () => {
|
const loadMoreModels = useCallback(async () => {
|
||||||
if (loading || !hasMore) return;
|
if (loading || !hasMore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
|
@ -187,9 +198,15 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
: searchParams.search;
|
: searchParams.search;
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (searchQuery) params.set('search', searchQuery);
|
if (searchQuery) {
|
||||||
if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag);
|
params.set('search', searchQuery);
|
||||||
if (searchParams.filter) params.set('filter', searchParams.filter);
|
}
|
||||||
|
if (searchParams.pipelineTag) {
|
||||||
|
params.set('pipeline_tag', searchParams.pipelineTag);
|
||||||
|
}
|
||||||
|
if (searchParams.filter) {
|
||||||
|
params.set('filter', searchParams.filter);
|
||||||
|
}
|
||||||
params.set('sort', sortBy);
|
params.set('sort', sortBy);
|
||||||
params.set('limit', String(MODELS_PER_PAGE));
|
params.set('limit', String(MODELS_PER_PAGE));
|
||||||
params.set('skip', String(pageRef.current * MODELS_PER_PAGE));
|
params.set('skip', String(pageRef.current * MODELS_PER_PAGE));
|
||||||
|
|
@ -220,16 +237,16 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: model.id,
|
|
||||||
name,
|
|
||||||
author,
|
author,
|
||||||
downloads: model.downloads ?? 0,
|
downloads: model.downloads ?? 0,
|
||||||
likes: model.likes ?? 0,
|
|
||||||
updatedAt: new Date(model.lastModified),
|
|
||||||
gated: model.gated ?? false,
|
gated: model.gated ?? false,
|
||||||
|
id: model.id,
|
||||||
|
likes: model.likes ?? 0,
|
||||||
|
name,
|
||||||
paramSize,
|
paramSize,
|
||||||
|
updatedAt: new Date(model.lastModified),
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
setModels((prev) => [...prev, ...results]);
|
setModels((prev) => [...prev, ...results]);
|
||||||
|
|
@ -283,13 +300,13 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
setLoadingFiles(false);
|
setLoadingFiles(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[searchParams?.filter]
|
[searchParams?.filter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getFileDownloadUrl = useCallback(
|
const getFileDownloadUrl = useCallback(
|
||||||
(modelId: string, filePath: string) =>
|
(modelId: string, filePath: string) =>
|
||||||
`${HUGGINGFACE_BASE_URL}/${modelId}/resolve/main/${filePath}`,
|
`${HUGGINGFACE_BASE_URL}/${modelId}/resolve/main/${filePath}`,
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
|
|
@ -302,31 +319,34 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
models,
|
changeSortOrder,
|
||||||
|
error,
|
||||||
files,
|
files,
|
||||||
|
getFileDownloadUrl,
|
||||||
|
hasMore,
|
||||||
|
loadModelFiles,
|
||||||
|
loadMoreModels,
|
||||||
loading,
|
loading,
|
||||||
loadingFiles,
|
loadingFiles,
|
||||||
error,
|
models,
|
||||||
hasMore,
|
reset,
|
||||||
|
searchModels,
|
||||||
|
searchParams,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
sortBy,
|
sortBy,
|
||||||
searchParams,
|
|
||||||
searchModels,
|
|
||||||
loadMoreModels,
|
|
||||||
loadModelFiles,
|
|
||||||
changeSortOrder,
|
|
||||||
getFileDownloadUrl,
|
|
||||||
reset,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExtensionsForLibrary = (filter?: string) => {
|
const getExtensionsForLibrary = (filter?: string) => {
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case 'gguf':
|
case 'gguf': {
|
||||||
return ['.gguf'];
|
return ['.gguf'];
|
||||||
case 'safetensors':
|
}
|
||||||
|
case 'safetensors': {
|
||||||
return ['.safetensors'];
|
return ['.safetensors'];
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
return ['.gguf', '.safetensors', '.bin', '.pt', '.pth'];
|
return ['.gguf', '.safetensors', '.bin', '.pt', '.pth'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import type { SdConvDirectMode } from '@/types';
|
import type { SdConvDirectMode } from '@/types';
|
||||||
|
|
||||||
interface UseLaunchLogicProps {
|
interface UseLaunchLogicProps {
|
||||||
|
|
@ -134,7 +135,9 @@ const buildConfigArgs = (isImageMode: boolean, launchArgs: LaunchArgs) => {
|
||||||
flagMappings.forEach(([condition, flag, value]) => {
|
flagMappings.forEach(([condition, flag, value]) => {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
args.push(flag);
|
args.push(flag);
|
||||||
if (value) args.push(value);
|
if (value) {
|
||||||
|
args.push(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -259,17 +262,17 @@ export const useLaunchLogic = ({ model, sdmodel, onLaunch }: UseLaunchLogicProps
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onLaunch();
|
onLaunch();
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = result.error || 'Unknown launch error';
|
const errorMessage = result.error ?? 'Unknown launch error';
|
||||||
window.electronAPI.logs.logError('Launch failed:', new Error(errorMessage));
|
window.electronAPI.logs.logError('Launch failed:', new Error(errorMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLaunching(false);
|
setIsLaunching(false);
|
||||||
},
|
},
|
||||||
[model, sdmodel, isLaunching, onLaunch]
|
[model, sdmodel, isLaunching, onLaunch],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLaunching,
|
|
||||||
handleLaunch,
|
handleLaunch,
|
||||||
|
isLaunching,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { initializeAudio, playSound, soundAssets } from '@/utils/sounds';
|
import { initializeAudio, playSound, soundAssets } from '@/utils/sounds';
|
||||||
|
|
||||||
export const useLogoClickSounds = () => {
|
export const useLogoClickSounds = () => {
|
||||||
|
|
@ -33,20 +34,20 @@ export const useLogoClickSounds = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLogoStyles = () => ({
|
const getLogoStyles = () => ({
|
||||||
cursor: 'pointer',
|
|
||||||
userSelect: 'none' as const,
|
|
||||||
transition: 'transform 0.15s ease-in-out',
|
|
||||||
transform: isElephantMode ? 'scale(1.3) rotate(5deg)' : 'scale(1) rotate(0deg)',
|
|
||||||
animation: isElephantMode
|
animation: isElephantMode
|
||||||
? 'elephantShake 1.5s ease-in-out'
|
? 'elephantShake 1.5s ease-in-out'
|
||||||
: isMouseSqueaking
|
: isMouseSqueaking
|
||||||
? 'mouseSqueak 0.3s ease-in-out'
|
? 'mouseSqueak 0.3s ease-in-out'
|
||||||
: 'none',
|
: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transform: isElephantMode ? 'scale(1.3) rotate(5deg)' : 'scale(1) rotate(0deg)',
|
||||||
|
transition: 'transform 0.15s ease-in-out',
|
||||||
|
userSelect: 'none' as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleLogoClick,
|
|
||||||
getLogoStyles,
|
getLogoStyles,
|
||||||
|
handleLogoClick,
|
||||||
isElephantMode,
|
isElephantMode,
|
||||||
isMouseSqueaking,
|
isMouseSqueaking,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import type { DismissedUpdate } from '@/types';
|
import type { DismissedUpdate } from '@/types';
|
||||||
import type { DownloadItem, InstalledBackend } from '@/types/electron';
|
import type { DownloadItem, InstalledBackend } from '@/types/electron';
|
||||||
|
|
@ -66,13 +67,13 @@ export const useUpdateChecker = () => {
|
||||||
const isUpdateDismissed = dismissedUpdates.some(
|
const isUpdateDismissed = dismissedUpdates.some(
|
||||||
(dismissedUpdate) =>
|
(dismissedUpdate) =>
|
||||||
dismissedUpdate.currentBackendPath === currentBackend.path &&
|
dismissedUpdate.currentBackendPath === currentBackend.path &&
|
||||||
dismissedUpdate.targetVersion === matchingDownload.version
|
dismissedUpdate.targetVersion === matchingDownload.version,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isUpdateDismissed) {
|
if (!isUpdateDismissed) {
|
||||||
setUpdateInfo({
|
setUpdateInfo({
|
||||||
currentBackend,
|
|
||||||
availableUpdate: matchingDownload,
|
availableUpdate: matchingDownload,
|
||||||
|
currentBackend,
|
||||||
});
|
});
|
||||||
setShowUpdateModal(true);
|
setShowUpdateModal(true);
|
||||||
}
|
}
|
||||||
|
|
@ -103,11 +104,11 @@ export const useUpdateChecker = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateInfo,
|
|
||||||
showUpdateModal,
|
|
||||||
isChecking,
|
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
skipUpdate,
|
|
||||||
closeModal,
|
closeModal,
|
||||||
|
isChecking,
|
||||||
|
showUpdateModal,
|
||||||
|
skipUpdate,
|
||||||
|
updateInfo,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
||||||
import type { CPUCapabilities, GPUDevice } from '@/types/hardware';
|
import type { CPUCapabilities, GPUDevice } from '@/types/hardware';
|
||||||
|
|
||||||
|
|
@ -24,16 +25,16 @@ const checkModelWarnings = (model: string, sdmodel: string, configLoaded: boolea
|
||||||
|
|
||||||
if (showModelPriorityWarning) {
|
if (showModelPriorityWarning) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'info',
|
|
||||||
message:
|
message:
|
||||||
'Both text and image generation models are selected. This may load both models into VRAM simultaneously, which requires significant memory (typically 16GB+ VRAM recommended). Ensure your system has sufficient VRAM to avoid crashes or poor performance.',
|
'Both text and image generation models are selected. This may load both models into VRAM simultaneously, which requires significant memory (typically 16GB+ VRAM recommended). Ensure your system has sufficient VRAM to avoid crashes or poor performance.',
|
||||||
|
type: 'info',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showNoModelWarning) {
|
if (showNoModelWarning) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'info',
|
|
||||||
message: 'Select a model in the General or Image Generation tab to enable launch.',
|
message: 'Select a model in the General or Image Generation tab to enable launch.',
|
||||||
|
type: 'info',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,15 +55,15 @@ interface GpuInfo {
|
||||||
const checkGpuWarnings = async (
|
const checkGpuWarnings = async (
|
||||||
accelerationSupport: AccelerationSupport,
|
accelerationSupport: AccelerationSupport,
|
||||||
gpuCapabilities: GpuCapabilities,
|
gpuCapabilities: GpuCapabilities,
|
||||||
gpuInfo: GpuInfo
|
gpuInfo: GpuInfo,
|
||||||
) => {
|
) => {
|
||||||
const warnings: Warning[] = [];
|
const warnings: Warning[] = [];
|
||||||
|
|
||||||
if (accelerationSupport.cuda && gpuCapabilities.cuda.devices.length === 0 && gpuInfo.hasNVIDIA) {
|
if (accelerationSupport.cuda && gpuCapabilities.cuda.devices.length === 0 && gpuInfo.hasNVIDIA) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'warning',
|
|
||||||
message:
|
message:
|
||||||
'Your binary supports CUDA and you have an NVIDIA GPU, but CUDA runtime is not detected on your system.',
|
'Your binary supports CUDA and you have an NVIDIA GPU, but CUDA runtime is not detected on your system.',
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,8 +79,8 @@ const checkGpuWarnings = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'info',
|
|
||||||
message,
|
message,
|
||||||
|
type: 'info',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,10 +97,10 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
|
||||||
if (gpuMemoryInfo) {
|
if (gpuMemoryInfo) {
|
||||||
const lowVramThreshold = 8;
|
const lowVramThreshold = 8;
|
||||||
const validGpus = gpuMemoryInfo.filter(
|
const validGpus = gpuMemoryInfo.filter(
|
||||||
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== ''
|
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== '',
|
||||||
);
|
);
|
||||||
const lowVramGpus = validGpus.filter(
|
const lowVramGpus = validGpus.filter(
|
||||||
(gpu) => parseFloat(gpu.totalMemoryGB ?? '0') < lowVramThreshold
|
(gpu) => parseFloat(gpu.totalMemoryGB ?? '0') < lowVramThreshold,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
|
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
|
||||||
|
|
@ -108,8 +109,8 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'warning',
|
|
||||||
message: `Low VRAM detected (${memoryDetails}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`,
|
message: `Low VRAM detected (${memoryDetails}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`,
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,11 +126,11 @@ const checkCpuWarnings = (acceleration: string, availableAccelerations: Accelera
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (availableAccelerations.length > 0 && availableAccelerations.some((a) => a.value === 'cpu')) {
|
if (availableAccelerations.some((a) => a.value === 'cpu')) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'info',
|
|
||||||
message:
|
message:
|
||||||
"LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.",
|
"LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.",
|
||||||
|
type: 'info',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,7 +182,7 @@ export const useWarnings = ({
|
||||||
|
|
||||||
const modelWarnings = useMemo(
|
const modelWarnings = useMemo(
|
||||||
() => checkModelWarnings(model, sdmodel, configLoaded),
|
() => checkModelWarnings(model, sdmodel, configLoaded),
|
||||||
[model, sdmodel, configLoaded]
|
[model, sdmodel, configLoaded],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateBackendWarnings = useCallback(async () => {
|
const updateBackendWarnings = useCallback(async () => {
|
||||||
|
|
@ -197,8 +198,8 @@ export const useWarnings = ({
|
||||||
|
|
||||||
const result = await checkBackendWarnings({
|
const result = await checkBackendWarnings({
|
||||||
acceleration,
|
acceleration,
|
||||||
cpuCapabilities: cpuCapabilitiesResult,
|
|
||||||
availableAccelerations,
|
availableAccelerations,
|
||||||
|
cpuCapabilities: cpuCapabilitiesResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
setBackendWarnings(result);
|
setBackendWarnings(result);
|
||||||
|
|
@ -210,7 +211,7 @@ export const useWarnings = ({
|
||||||
|
|
||||||
const allWarnings = useMemo(
|
const allWarnings = useMemo(
|
||||||
() => [...modelWarnings, ...backendWarnings],
|
() => [...modelWarnings, ...backendWarnings],
|
||||||
[modelWarnings, backendWarnings]
|
[modelWarnings, backendWarnings],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { App } from '@/components/App';
|
import { App } from '@/components/App';
|
||||||
import { cssVariablesResolver, theme } from '@/theme';
|
import { cssVariablesResolver, theme } from '@/theme';
|
||||||
|
|
||||||
import '@/styles/index.css';
|
import '@/styles/index.css';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (!rootElement) throw new Error('Root element not found');
|
if (!rootElement) {
|
||||||
|
throw new Error('Root element not found');
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
@ -17,5 +21,5 @@ createRoot(rootElement).render(
|
||||||
>
|
>
|
||||||
<App />
|
<App />
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { exit, platform, stderr, stdin, stdout } from 'node:process';
|
import { exit, platform, stderr, stdin, stdout } from 'node:process';
|
||||||
|
|
||||||
import { pathExists, readJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile } from '@/utils/node/fs';
|
||||||
import { getConfigDir } from '@/utils/node/path';
|
import { getConfigDir } from '@/utils/node/path';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
|
|
@ -12,7 +13,7 @@ async function getCurrentKoboldBinary() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await readJsonFile<{ currentKoboldBinary?: string }>(configPath);
|
const config = await readJsonFile<{ currentKoboldBinary?: string }>(configPath);
|
||||||
return config?.currentKoboldBinary || null;
|
return config?.currentKoboldBinary ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -36,8 +37,8 @@ export async function handleCliMode(args: string[]) {
|
||||||
const isWindows = platform === 'win32';
|
const isWindows = platform === 'win32';
|
||||||
|
|
||||||
const child = spawn(currentBinary, args, {
|
const child = spawn(currentBinary, args, {
|
||||||
stdio: isWindows ? 'pipe' : 'inherit',
|
|
||||||
detached: false,
|
detached: false,
|
||||||
|
stdio: isWindows ? 'pipe' : 'inherit',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import { setupIPCHandlers } from '@/main/ipc';
|
import { setupIPCHandlers } from '@/main/ipc';
|
||||||
import {
|
import {
|
||||||
|
|
@ -77,7 +79,7 @@ export async function initializeApp(options?: { startMinimized?: boolean }) {
|
||||||
const timeoutPromise = new Promise<void>((resolve) => {
|
const timeoutPromise = new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, 10000);
|
}, 10_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.race([Promise.all(cleanupPromises), timeoutPromise]);
|
await Promise.race([Promise.all(cleanupPromises), timeoutPromise]);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
canAutoUpdate,
|
canAutoUpdate,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
|
|
@ -78,7 +80,7 @@ export function setupIPCHandlers() {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
|
|
||||||
ipcMain.handle('kobold:downloadRelease', async (_, asset, options) =>
|
ipcMain.handle('kobold:downloadRelease', async (_, asset, options) =>
|
||||||
downloadRelease(asset, options)
|
downloadRelease(asset, options),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getInstalledBackends', () => getInstalledBackends());
|
ipcMain.handle('kobold:getInstalledBackends', () => getInstalledBackends());
|
||||||
|
|
@ -88,7 +90,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('kobold:getConfigFiles', () => getConfigFiles());
|
ipcMain.handle('kobold:getConfigFiles', () => getConfigFiles());
|
||||||
|
|
||||||
ipcMain.handle('kobold:saveConfigFile', async (_, configName, configData) =>
|
ipcMain.handle('kobold:saveConfigFile', async (_, configName, configData) =>
|
||||||
saveConfigFile(configName, configData)
|
saveConfigFile(configName, configData),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => deleteConfigFile(configName));
|
ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => deleteConfigFile(configName));
|
||||||
|
|
@ -96,7 +98,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig());
|
ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig());
|
||||||
|
|
||||||
ipcMain.handle('kobold:setSelectedConfig', (_, configName) =>
|
ipcMain.handle('kobold:setSelectedConfig', (_, configName) =>
|
||||||
setConfig('selectedConfig', configName)
|
setConfig('selectedConfig', configName),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:setCurrentBackend', (_, version) => setCurrentBackend(version));
|
ipcMain.handle('kobold:setCurrentBackend', (_, version) => setCurrentBackend(version));
|
||||||
|
|
@ -120,13 +122,13 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('kobold:detectAccelerationSupport', () => detectAccelerationSupport());
|
ipcMain.handle('kobold:detectAccelerationSupport', () => detectAccelerationSupport());
|
||||||
|
|
||||||
ipcMain.handle('kobold:getAvailableAccelerations', (_, includeDisabled = false) =>
|
ipcMain.handle('kobold:getAvailableAccelerations', (_, includeDisabled = false) =>
|
||||||
getAvailableAccelerations(includeDisabled)
|
getAvailableAccelerations(includeDisabled),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getPlatform', () => platform);
|
ipcMain.handle('kobold:getPlatform', () => platform);
|
||||||
|
|
||||||
ipcMain.handle('kobold:launchKoboldCpp', (_, args, preLaunchCommands) =>
|
ipcMain.handle('kobold:launchKoboldCpp', (_, args, preLaunchCommands) =>
|
||||||
launchKoboldCppWithCustomFrontends(args, preLaunchCommands)
|
launchKoboldCppWithCustomFrontends(args, preLaunchCommands),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => deleteRelease(binaryPath));
|
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => deleteRelease(binaryPath));
|
||||||
|
|
@ -144,7 +146,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
|
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
|
||||||
|
|
||||||
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
|
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
|
||||||
getLocalModelsForType(paramType as Parameters<typeof getLocalModelsForType>[0])
|
getLocalModelsForType(paramType as Parameters<typeof getLocalModelsForType>[0]),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => analyzeGGUFModel(filePath));
|
ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => analyzeGGUFModel(filePath));
|
||||||
|
|
@ -157,15 +159,15 @@ export function setupIPCHandlers() {
|
||||||
contextSize: number,
|
contextSize: number,
|
||||||
availableVramGB: number,
|
availableVramGB: number,
|
||||||
flashAttention: boolean,
|
flashAttention: boolean,
|
||||||
acceleration: Acceleration
|
acceleration: Acceleration,
|
||||||
) =>
|
) =>
|
||||||
calculateOptimalGpuLayers({
|
calculateOptimalGpuLayers({
|
||||||
modelPath,
|
|
||||||
contextSize,
|
|
||||||
availableVramGB,
|
|
||||||
flashAttention,
|
|
||||||
acceleration,
|
acceleration,
|
||||||
})
|
availableVramGB,
|
||||||
|
contextSize,
|
||||||
|
flashAttention,
|
||||||
|
modelPath,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('config:get', (_, key) => getConfig(key));
|
ipcMain.handle('config:get', (_, key) => getConfig(key));
|
||||||
|
|
@ -179,7 +181,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('app:openPath', async (_, path) => openPathHandler(path));
|
ipcMain.handle('app:openPath', async (_, path) => openPathHandler(path));
|
||||||
|
|
||||||
ipcMain.handle('app:showLogsFolder', () =>
|
ipcMain.handle('app:showLogsFolder', () =>
|
||||||
openPathHandler(join(app.getPath('userData'), 'logs'))
|
openPathHandler(join(app.getPath('userData'), 'logs')),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('app:viewConfigFile', () => openPathHandler(getConfigDir()));
|
ipcMain.handle('app:viewConfigFile', () => openPathHandler(getConfigDir()));
|
||||||
|
|
@ -208,7 +210,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('app:getColorScheme', () => getColorScheme());
|
ipcMain.handle('app:getColorScheme', () => getColorScheme());
|
||||||
|
|
||||||
ipcMain.handle('app:setColorScheme', async (_, colorScheme) =>
|
ipcMain.handle('app:setColorScheme', async (_, colorScheme) =>
|
||||||
setConfig('colorScheme', colorScheme)
|
setConfig('colorScheme', colorScheme),
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('app:getEnableSystemTray', () => getEnableSystemTray());
|
ipcMain.handle('app:getEnableSystemTray', () => getEnableSystemTray());
|
||||||
|
|
@ -237,10 +239,10 @@ export function setupIPCHandlers() {
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
config?: string | null;
|
config?: string | null;
|
||||||
monitoringEnabled?: boolean;
|
monitoringEnabled?: boolean;
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
updateTrayState(state);
|
updateTrayState(state);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('app:openExternal', async (_, url) => openUrl(url));
|
ipcMain.handle('app:openExternal', async (_, url) => openUrl(url));
|
||||||
|
|
@ -264,11 +266,11 @@ export function setupIPCHandlers() {
|
||||||
const { rm } = await import('node:fs/promises');
|
const { rm } = await import('node:fs/promises');
|
||||||
const openWebUIDataDir = join(getInstallDir(), 'openwebui-data');
|
const openWebUIDataDir = join(getInstallDir(), 'openwebui-data');
|
||||||
try {
|
try {
|
||||||
await rm(openWebUIDataDir, { recursive: true, force: true });
|
await rm(openWebUIDataDir, { force: true, recursive: true });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to clear Open WebUI data:', error as Error);
|
logError('Failed to clear Open WebUI data:', error as Error);
|
||||||
return { success: false, error: (error as Error).message };
|
return { error: (error as Error).message, success: false };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
|
||||||
import { isDevelopment } from '@/utils/node/environment';
|
import { isDevelopment } from '@/utils/node/environment';
|
||||||
import { logError, safeExecute } from '@/utils/node/logging';
|
import { logError, safeExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { getAURVersion, isWindowsPortableInstallation } from './dependencies';
|
import { getAURVersion, isWindowsPortableInstallation } from './dependencies';
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
|
|
@ -67,7 +70,9 @@ export function quitAndInstall() {
|
||||||
export const isUpdateDownloaded = () => updateDownloaded;
|
export const isUpdateDownloaded = () => updateDownloaded;
|
||||||
|
|
||||||
export const canAutoUpdate = async () => {
|
export const canAutoUpdate = async () => {
|
||||||
if (!app.isPackaged) return false;
|
if (!app.isPackaged) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (platform === 'linux' && (await getAURVersion()) !== null) {
|
if (platform === 'linux' && (await getAURVersion()) !== null) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
import { nativeTheme } from 'electron';
|
import { nativeTheme } from 'electron';
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
DismissedUpdate,
|
DismissedUpdate,
|
||||||
|
|
@ -43,11 +45,11 @@ let config: AppConfig = {};
|
||||||
let configPath: string;
|
let configPath: string;
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
const config = await safeExecute(
|
const loaded = await safeExecute(
|
||||||
() => readJsonFile<AppConfig>(configPath),
|
() => readJsonFile<AppConfig>(configPath),
|
||||||
'Error loading config'
|
'Error loading config',
|
||||||
);
|
);
|
||||||
return config || {};
|
return loaded ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
|
|
@ -71,16 +73,19 @@ function getDefaultInstallDir() {
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'win32':
|
case 'win32': {
|
||||||
return join(home, PRODUCT_NAME);
|
return join(home, PRODUCT_NAME);
|
||||||
case 'darwin':
|
}
|
||||||
|
case 'darwin': {
|
||||||
return join(home, 'Applications', PRODUCT_NAME);
|
return join(home, 'Applications', PRODUCT_NAME);
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
return join(home, '.local', 'share', PRODUCT_NAME);
|
return join(home, '.local', 'share', PRODUCT_NAME);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getInstallDir = () => config.installDir || getDefaultInstallDir();
|
export const getInstallDir = () => config.installDir ?? getDefaultInstallDir();
|
||||||
|
|
||||||
export async function setInstallDir(dir: string) {
|
export async function setInstallDir(dir: string) {
|
||||||
config.installDir = dir;
|
config.installDir = dir;
|
||||||
|
|
@ -99,7 +104,7 @@ export async function setCurrentKoboldBinary(binaryPath: string) {
|
||||||
|
|
||||||
export const getSelectedConfig = () => config.selectedConfig;
|
export const getSelectedConfig = () => config.selectedConfig;
|
||||||
|
|
||||||
export const getColorScheme = () => config.colorScheme || 'auto';
|
export const getColorScheme = () => config.colorScheme ?? 'auto';
|
||||||
|
|
||||||
export function getBackgroundColor() {
|
export function getBackgroundColor() {
|
||||||
const colorScheme = getColorScheme();
|
const colorScheme = getColorScheme();
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import { access, readdir, readlink } from 'node:fs/promises';
|
||||||
import { homedir, release } from 'node:os';
|
import { homedir, release } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { arch, platform, env as processEnv, versions } from 'node:process';
|
import { arch, platform, env as processEnv, versions } from 'node:process';
|
||||||
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
|
||||||
const PATH_SEPARATOR = platform === 'win32' ? ';' : ':';
|
const PATH_SEPARATOR = platform === 'win32' ? ';' : ':';
|
||||||
|
|
@ -31,15 +33,15 @@ async function executeCommand(
|
||||||
command: string,
|
command: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
timeout = 5000
|
timeout = 5000,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execa(command, args, {
|
const { stdout } = await execa(command, args, {
|
||||||
env,
|
env,
|
||||||
timeout,
|
|
||||||
reject: false,
|
reject: false,
|
||||||
|
timeout,
|
||||||
});
|
});
|
||||||
return { success: true, output: stdout.trim() };
|
return { output: stdout.trim(), success: true };
|
||||||
} catch {
|
} catch {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +70,7 @@ export async function getSystemNodeVersion() {
|
||||||
|
|
||||||
export async function isUvAvailable() {
|
export async function isUvAvailable() {
|
||||||
const env = await getUvEnvironment();
|
const env = await getUvEnvironment();
|
||||||
const result = await executeCommand('uv', ['--version'], env, 10000);
|
const result = await executeCommand('uv', ['--version'], env, 10_000);
|
||||||
return result.success;
|
return result.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +91,7 @@ export async function getUvEnvironment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
env.PYTHONIOENCODING = 'utf-8';
|
env.PYTHONIOENCODING = 'utf8';
|
||||||
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
||||||
env.PYTHONUTF8 = '1';
|
env.PYTHONUTF8 = '1';
|
||||||
env.CHCP = '65001';
|
env.CHCP = '65001';
|
||||||
|
|
@ -169,7 +171,7 @@ async function tryVersionManagerPath(basePath: string, env: Record<string, strin
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = await readdir(basePath);
|
const entries = await readdir(basePath);
|
||||||
const nodeVersions = entries.filter((v) => v.startsWith('v')).sort();
|
const nodeVersions = entries.filter((v) => v.startsWith('v')).toSorted();
|
||||||
const latestVersion = nodeVersions.pop();
|
const latestVersion = nodeVersions.pop();
|
||||||
|
|
||||||
if (latestVersion) {
|
if (latestVersion) {
|
||||||
|
|
@ -198,8 +200,8 @@ export async function getAURVersion() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execa('pacman', ['-Q', packageName], {
|
const { stdout } = await execa('pacman', ['-Q', packageName], {
|
||||||
timeout: 1000,
|
|
||||||
reject: false,
|
reject: false,
|
||||||
|
timeout: 1000,
|
||||||
});
|
});
|
||||||
const trimmed = stdout.trim();
|
const trimmed = stdout.trim();
|
||||||
if (trimmed.length > 0) {
|
if (trimmed.length > 0) {
|
||||||
|
|
@ -226,16 +228,16 @@ export async function getVersionInfo() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appVersion: app.getVersion(),
|
appVersion: app.getVersion(),
|
||||||
electronVersion: versions.electron,
|
arch,
|
||||||
nodeVersion: versions.node,
|
aurPackageVersion,
|
||||||
chromeVersion: versions.chrome,
|
chromeVersion: versions.chrome,
|
||||||
v8Version: versions.v8,
|
electronVersion: versions.electron,
|
||||||
|
nodeJsSystemVersion,
|
||||||
|
nodeVersion: versions.node,
|
||||||
osVersion: release(),
|
osVersion: release(),
|
||||||
platform,
|
platform,
|
||||||
arch,
|
|
||||||
nodeJsSystemVersion,
|
|
||||||
uvVersion,
|
uvVersion,
|
||||||
aurPackageVersion,
|
v8Version: versions.v8,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +267,7 @@ export function isWindowsPortableInstallation() {
|
||||||
execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\tmp\\');
|
execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\tmp\\');
|
||||||
|
|
||||||
const isInProgramFiles = execPath.toLowerCase().includes('program files');
|
const isInProgramFiles = execPath.toLowerCase().includes('program files');
|
||||||
const isInAppDataPrograms = execPath.toLowerCase().includes('appdata\\local\\programs');
|
const isInAppDataPrograms = execPath.toLowerCase().includes(String.raw`appdata\local\programs`);
|
||||||
|
|
||||||
windowsPortableInstallationCache = isInTemp && !isInProgramFiles && !isInAppDataPrograms;
|
windowsPortableInstallationCache = isInTemp && !isInProgramFiles && !isInAppDataPrograms;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation';
|
import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BasicGPUInfo,
|
BasicGPUInfo,
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
|
|
@ -14,8 +16,8 @@ import { safeExecute } from '@/utils/node/logging';
|
||||||
import { detectGPUViaVulkan, getVulkanInfo } from '@/utils/node/vulkan';
|
import { detectGPUViaVulkan, getVulkanInfo } from '@/utils/node/vulkan';
|
||||||
|
|
||||||
const COMMON_EXEC_OPTIONS = {
|
const COMMON_EXEC_OPTIONS = {
|
||||||
timeout: 3000,
|
|
||||||
reject: false,
|
reject: false,
|
||||||
|
timeout: 3000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cpuCapabilitiesCache: CPUCapabilities | null = null;
|
let cpuCapabilitiesCache: CPUCapabilities | null = null;
|
||||||
|
|
@ -36,8 +38,8 @@ export async function detectCPU() {
|
||||||
const name = formatDeviceName(cpu.brand);
|
const name = formatDeviceName(cpu.brand);
|
||||||
|
|
||||||
devices.push({
|
devices.push({
|
||||||
name,
|
|
||||||
detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`,
|
detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`,
|
||||||
|
name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +51,7 @@ export async function detectCPU() {
|
||||||
return capabilities;
|
return capabilities;
|
||||||
}, 'CPU detection failed');
|
}, 'CPU detection failed');
|
||||||
|
|
||||||
cpuCapabilitiesCache = result || {
|
cpuCapabilitiesCache = result ?? {
|
||||||
devices: [],
|
devices: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,12 +66,12 @@ export async function detectGPU() {
|
||||||
const result = await safeExecute(() => detectGPUViaVulkan(), 'GPU detection failed');
|
const result = await safeExecute(() => detectGPUViaVulkan(), 'GPU detection failed');
|
||||||
|
|
||||||
const fallbackGPUInfo = {
|
const fallbackGPUInfo = {
|
||||||
|
gpuInfo: [],
|
||||||
hasAMD: false,
|
hasAMD: false,
|
||||||
hasNVIDIA: false,
|
hasNVIDIA: false,
|
||||||
gpuInfo: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
basicGPUInfoCache = result || fallbackGPUInfo;
|
basicGPUInfoCache = result ?? fallbackGPUInfo;
|
||||||
return basicGPUInfoCache;
|
return basicGPUInfoCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,17 +94,17 @@ async function detectVulkan() {
|
||||||
const devices: GPUDevice[] = [];
|
const devices: GPUDevice[] = [];
|
||||||
|
|
||||||
for (const gpu of vulkanInfo.allGPUs) {
|
for (const gpu of vulkanInfo.allGPUs) {
|
||||||
const isIntegrated = gpu.isIntegrated;
|
const { isIntegrated } = gpu;
|
||||||
|
|
||||||
devices.push({
|
devices.push({
|
||||||
name: isIntegrated ? gpu.name : formatDeviceName(gpu.name),
|
|
||||||
isIntegrated,
|
isIntegrated,
|
||||||
|
name: isIntegrated ? gpu.name : formatDeviceName(gpu.name),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
devices,
|
devices,
|
||||||
version: vulkanInfo.apiVersion || 'Unknown',
|
version: vulkanInfo.apiVersion ?? 'Unknown',
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { devices: [], version: 'Unknown' };
|
return { devices: [], version: 'Unknown' };
|
||||||
|
|
@ -114,7 +116,7 @@ async function detectCUDA() {
|
||||||
const { stdout } = await execa(
|
const { stdout } = await execa(
|
||||||
'nvidia-smi',
|
'nvidia-smi',
|
||||||
['--query-gpu=name,driver_version', '--format=csv,noheader'],
|
['--query-gpu=name,driver_version', '--format=csv,noheader'],
|
||||||
COMMON_EXEC_OPTIONS
|
COMMON_EXEC_OPTIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (stdout.trim()) {
|
if (stdout.trim()) {
|
||||||
|
|
@ -127,7 +129,7 @@ async function detectCUDA() {
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasError = errorPatterns.some((pattern) =>
|
const hasError = errorPatterns.some((pattern) =>
|
||||||
stdout.toLowerCase().includes(pattern.toLowerCase())
|
stdout.toLowerCase().includes(pattern.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
|
|
@ -194,7 +196,7 @@ export async function detectROCm() {
|
||||||
'-Command',
|
'-Command',
|
||||||
`Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Name -like '*AMD*' -or $_.Name -like '*Radeon*' } | Select-Object -First 1 -ExpandProperty DriverVersion`,
|
`Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Name -like '*AMD*' -or $_.Name -like '*Radeon*' } | Select-Object -First 1 -ExpandProperty DriverVersion`,
|
||||||
],
|
],
|
||||||
COMMON_EXEC_OPTIONS
|
COMMON_EXEC_OPTIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (driverOutput.trim()) {
|
if (driverOutput.trim()) {
|
||||||
|
|
@ -214,8 +216,8 @@ export async function detectROCm() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
devices,
|
devices,
|
||||||
version,
|
|
||||||
driverVersion,
|
driverVersion,
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +250,7 @@ function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentDevice?.name) {
|
if (currentDevice?.name) {
|
||||||
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false));
|
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated ?? false));
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices;
|
return devices;
|
||||||
|
|
@ -257,11 +259,11 @@ function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
||||||
function handleHipInfoLine(
|
function handleHipInfoLine(
|
||||||
trimmedLine: string,
|
trimmedLine: string,
|
||||||
currentDevice: Partial<GPUDevice> | null,
|
currentDevice: Partial<GPUDevice> | null,
|
||||||
devices: GPUDevice[]
|
devices: GPUDevice[],
|
||||||
) {
|
) {
|
||||||
if (trimmedLine.startsWith('device#')) {
|
if (trimmedLine.startsWith('device#')) {
|
||||||
if (currentDevice?.name) {
|
if (currentDevice?.name) {
|
||||||
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false));
|
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated ?? false));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -283,21 +285,25 @@ function parseRocmInfoDevice(
|
||||||
line: string,
|
line: string,
|
||||||
lines: string[],
|
lines: string[],
|
||||||
index: number,
|
index: number,
|
||||||
vulkanInfo: { allGPUs: GPUDevice[] }
|
vulkanInfo: { allGPUs: GPUDevice[] },
|
||||||
) {
|
) {
|
||||||
const name = line.split('Marketing Name:')[1]?.trim();
|
const name = line.split('Marketing Name:')[1]?.trim();
|
||||||
if (!name) return null;
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const deviceType = findDeviceType(lines, index);
|
const deviceType = findDeviceType(lines, index);
|
||||||
if (deviceType === 'CPU') return null;
|
if (deviceType === 'CPU') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isIntegrated = determineIfIntegrated(name, vulkanInfo);
|
const isIntegrated = determineIfIntegrated(name, vulkanInfo);
|
||||||
return createDevice(name, isIntegrated);
|
return createDevice(name, isIntegrated);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDevice = (name: string, isIntegrated: boolean) => ({
|
const createDevice = (name: string, isIntegrated: boolean) => ({
|
||||||
name: isIntegrated ? name : formatDeviceName(name),
|
|
||||||
isIntegrated,
|
isIntegrated,
|
||||||
|
name: isIntegrated ? name : formatDeviceName(name),
|
||||||
});
|
});
|
||||||
|
|
||||||
function findDeviceType(lines: string[], startIndex: number) {
|
function findDeviceType(lines: string[], startIndex: number) {
|
||||||
|
|
@ -316,7 +322,7 @@ function findDeviceType(lines: string[], startIndex: number) {
|
||||||
function determineIfIntegrated(name: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
function determineIfIntegrated(name: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
||||||
try {
|
try {
|
||||||
const matchingGPU = vulkanInfo.allGPUs.find(
|
const matchingGPU = vulkanInfo.allGPUs.find(
|
||||||
(gpu) => gpu.name.includes(name) || name.includes(gpu.name)
|
(gpu) => gpu.name.includes(name) || name.includes(gpu.name),
|
||||||
);
|
);
|
||||||
return matchingGPU ? matchingGPU.isIntegrated : false;
|
return matchingGPU ? matchingGPU.isIntegrated : false;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -341,14 +347,14 @@ export async function detectGPUMemory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryInfo.push({
|
memoryInfo.push({
|
||||||
totalMemoryGB: vram?.toFixed(2) || null,
|
totalMemoryGB: vram?.toFixed(2) ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return memoryInfo;
|
return memoryInfo;
|
||||||
}, 'GPU memory detection failed');
|
}, 'GPU memory detection failed');
|
||||||
|
|
||||||
gpuMemoryInfoCache = result || [];
|
gpuMemoryInfoCache = result ?? [];
|
||||||
return gpuMemoryInfoCache;
|
return gpuMemoryInfoCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,7 +371,7 @@ export const detectSystemMemory = async () => {
|
||||||
const populatedSlot = memLayout.find(
|
const populatedSlot = memLayout.find(
|
||||||
(slot) =>
|
(slot) =>
|
||||||
slot.size > 0 &&
|
slot.size > 0 &&
|
||||||
((slot.clockSpeed && slot.clockSpeed > 0) || (slot.type && slot.type !== ''))
|
((slot.clockSpeed && slot.clockSpeed > 0) ?? (slot.type && slot.type !== '')),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (populatedSlot) {
|
if (populatedSlot) {
|
||||||
|
|
@ -378,13 +384,14 @@ export const detectSystemMemory = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalGB,
|
|
||||||
speed,
|
speed,
|
||||||
|
totalGB,
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
|
const mem = await siMem();
|
||||||
return {
|
return {
|
||||||
totalGB: ((await siMem()).total / 1024 ** 3).toFixed(2),
|
totalGB: (mem.total / 1024 ** 3).toFixed(2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { detectCPU, detectGPUCapabilities } from '../hardware';
|
import { detectCPU, detectGPUCapabilities } from '../hardware';
|
||||||
import { getCurrentBinaryInfo } from './backend';
|
import { getCurrentBinaryInfo } from './backend';
|
||||||
|
|
||||||
|
|
@ -18,11 +20,11 @@ async function detectAccelerationSupportFromPath(koboldBinaryPath: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const support: AccelerationSupport = {
|
const support: AccelerationSupport = {
|
||||||
|
cuda: false,
|
||||||
|
failsafe: false,
|
||||||
|
noavx2: false,
|
||||||
rocm: false,
|
rocm: false,
|
||||||
vulkan: false,
|
vulkan: false,
|
||||||
noavx2: false,
|
|
||||||
failsafe: false,
|
|
||||||
cuda: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await tryExecute(async () => {
|
await tryExecute(async () => {
|
||||||
|
|
@ -75,11 +77,11 @@ export const detectAccelerationSupport = async () =>
|
||||||
}
|
}
|
||||||
|
|
||||||
return detectAccelerationSupportFromPath(currentBinaryInfo.path);
|
return detectAccelerationSupportFromPath(currentBinaryInfo.path);
|
||||||
}, 'Error detecting current binary acceleration support')) || null;
|
}, 'Error detecting current binary acceleration support')) ?? null;
|
||||||
|
|
||||||
export async function getAvailableAccelerations(includeDisabled = false) {
|
export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
return [{ value: 'cpu', label: CPU_LABEL }];
|
return [{ label: CPU_LABEL, value: 'cpu' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await safeExecute(async () => {
|
const result = await safeExecute(async () => {
|
||||||
|
|
@ -90,7 +92,7 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!currentBinaryInfo?.path) {
|
if (!currentBinaryInfo?.path) {
|
||||||
return [{ value: 'cpu', label: CPU_LABEL }];
|
return [{ label: CPU_LABEL, value: 'cpu' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
|
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
|
||||||
|
|
@ -112,10 +114,10 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
const isSupported = hardwareCapabilities.cuda.devices.length > 0;
|
const isSupported = hardwareCapabilities.cuda.devices.length > 0;
|
||||||
if (isSupported || includeDisabled) {
|
if (isSupported || includeDisabled) {
|
||||||
accelerations.push({
|
accelerations.push({
|
||||||
value: 'cuda',
|
|
||||||
label: 'CUDA',
|
|
||||||
devices: hardwareCapabilities.cuda.devices,
|
devices: hardwareCapabilities.cuda.devices,
|
||||||
disabled: includeDisabled ? !isSupported : undefined,
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
label: 'CUDA',
|
||||||
|
value: 'cuda',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,10 +126,10 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
const isSupported = hardwareCapabilities.rocm.devices.length > 0;
|
const isSupported = hardwareCapabilities.rocm.devices.length > 0;
|
||||||
if (isSupported || includeDisabled) {
|
if (isSupported || includeDisabled) {
|
||||||
accelerations.push({
|
accelerations.push({
|
||||||
value: 'rocm',
|
|
||||||
label: 'ROCm',
|
|
||||||
devices: hardwareCapabilities.rocm.devices,
|
devices: hardwareCapabilities.rocm.devices,
|
||||||
disabled: includeDisabled ? !isSupported : undefined,
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
label: 'ROCm',
|
||||||
|
value: 'rocm',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,24 +138,26 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
const isSupported = hardwareCapabilities.vulkan.devices.length > 0;
|
const isSupported = hardwareCapabilities.vulkan.devices.length > 0;
|
||||||
if (isSupported || includeDisabled) {
|
if (isSupported || includeDisabled) {
|
||||||
accelerations.push({
|
accelerations.push({
|
||||||
value: 'vulkan',
|
|
||||||
label: 'Vulkan',
|
|
||||||
devices: hardwareCapabilities.vulkan.devices,
|
devices: hardwareCapabilities.vulkan.devices,
|
||||||
disabled: includeDisabled ? !isSupported : undefined,
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
label: 'Vulkan',
|
||||||
|
value: 'vulkan',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accelerations.push({
|
accelerations.push({
|
||||||
value: 'cpu',
|
devices: cpuCapabilities?.devices.map((device) => device.name) ?? [],
|
||||||
label: CPU_LABEL,
|
|
||||||
devices: cpuCapabilities?.devices.map((device) => device.name) || [],
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
label: CPU_LABEL,
|
||||||
|
value: 'cpu',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (includeDisabled) {
|
if (includeDisabled) {
|
||||||
accelerations.sort((a, b) => {
|
accelerations.sort((a, b) => {
|
||||||
if (a.disabled === b.disabled) return 0;
|
if (a.disabled === b.disabled) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return a.disabled ? 1 : -1;
|
return a.disabled ? 1 : -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -162,5 +166,5 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
return accelerations;
|
return accelerations;
|
||||||
}, 'Failed to get available accelerations');
|
}, 'Failed to get available accelerations');
|
||||||
|
|
||||||
return result || [{ value: 'cpu', label: CPU_LABEL }];
|
return result ?? [{ label: CPU_LABEL, value: 'cpu' }];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
|
|
||||||
import { gguf } from '@huggingface/gguf';
|
import { gguf } from '@huggingface/gguf';
|
||||||
|
|
||||||
import { formatBytes } from '@/utils/format';
|
import { formatBytes } from '@/utils/format';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
|
|
||||||
|
|
@ -17,23 +19,39 @@ function estimateMemoryRequirements(fileSize: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function estimateVramPerLayer(fileSize: number, layers?: number) {
|
function estimateVramPerLayer(fileSize: number, layers?: number) {
|
||||||
if (!layers || layers === 0) return undefined;
|
if (!layers || layers === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const perLayer = fileSize / layers;
|
const perLayer = fileSize / layers;
|
||||||
return formatBytes(perLayer);
|
return formatBytes(perLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatParameterCount(params?: number) {
|
function formatParameterCount(params?: number) {
|
||||||
if (!params) return undefined;
|
if (!params) {
|
||||||
if (params >= 1e9) return `${(params / 1e9).toFixed(1)}B`;
|
return undefined;
|
||||||
if (params >= 1e6) return `${(params / 1e6).toFixed(1)}M`;
|
}
|
||||||
if (params >= 1e3) return `${(params / 1e3).toFixed(1)}K`;
|
if (params >= 1e9) {
|
||||||
|
return `${(params / 1e9).toFixed(1)}B`;
|
||||||
|
}
|
||||||
|
if (params >= 1e6) {
|
||||||
|
return `${(params / 1e6).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (params >= 1e3) {
|
||||||
|
return `${(params / 1e3).toFixed(1)}K`;
|
||||||
|
}
|
||||||
return params.toString();
|
return params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContextLength(length?: number) {
|
function formatContextLength(length?: number) {
|
||||||
if (!length) return undefined;
|
if (!length) {
|
||||||
if (length >= 1e6) return `${(length / 1e6).toFixed(1)}M`;
|
return undefined;
|
||||||
if (length >= 1e3) return `${(length / 1e3).toFixed(0)}K`;
|
}
|
||||||
|
if (length >= 1e6) {
|
||||||
|
return `${(length / 1e6).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (length >= 1e3) {
|
||||||
|
return `${(length / 1e3).toFixed(0)}K`;
|
||||||
|
}
|
||||||
return length.toString();
|
return length.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,29 +100,30 @@ export async function analyzeGGUFModel(filePath: string) {
|
||||||
const vramPerLayer = estimateVramPerLayer(fileSize, blockCount);
|
const vramPerLayer = estimateVramPerLayer(fileSize, blockCount);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
general: {
|
|
||||||
architecture,
|
|
||||||
name,
|
|
||||||
fileSize: formatBytes(fileSize),
|
|
||||||
parameterCount: formatParameterCount(paramCount),
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
maxContextLength: formatContextLength(contextLength),
|
|
||||||
},
|
|
||||||
architecture: {
|
architecture: {
|
||||||
layers: blockCount,
|
layers: blockCount,
|
||||||
expertCount,
|
expertCount,
|
||||||
},
|
},
|
||||||
|
context: {
|
||||||
|
maxContextLength: formatContextLength(contextLength),
|
||||||
|
},
|
||||||
estimates: {
|
estimates: {
|
||||||
fullGpuVram: memoryEstimates.fullGpuVram,
|
fullGpuVram: memoryEstimates.fullGpuVram,
|
||||||
systemRam: memoryEstimates.systemRam,
|
systemRam: memoryEstimates.systemRam,
|
||||||
vramPerLayer,
|
vramPerLayer,
|
||||||
},
|
},
|
||||||
|
general: {
|
||||||
|
architecture,
|
||||||
|
name,
|
||||||
|
fileSize: formatBytes(fileSize),
|
||||||
|
parameterCount: formatParameterCount(paramCount),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Error analyzing GGUF model:', error as Error);
|
logError('Error analyzing GGUF model:', error as Error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to analyze model: ${error instanceof Error ? error.message : 'Unknown error'}`
|
`Failed to analyze model: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
{ cause: error },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { readdir, rm, stat } from 'node:fs/promises';
|
import { readdir, rm, stat } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
|
|
||||||
import type { InstalledBackend } from '@/types/electron';
|
import type { InstalledBackend } from '@/types/electron';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
import { getLauncherPath } from '@/utils/node/path';
|
import { getLauncherPath } from '@/utils/node/path';
|
||||||
|
|
||||||
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
|
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
|
||||||
import { sendToRenderer } from '../window';
|
import { sendToRenderer } from '../window';
|
||||||
|
|
||||||
|
|
@ -36,10 +39,10 @@ export async function getInstalledBackends() {
|
||||||
const launcherPath = await getLauncherPath(itemPath);
|
const launcherPath = await getLauncherPath(itemPath);
|
||||||
if (launcherPath && (await pathExists(launcherPath))) {
|
if (launcherPath && (await pathExists(launcherPath))) {
|
||||||
const launcherStats = await stat(launcherPath);
|
const launcherStats = await stat(launcherPath);
|
||||||
const launcherFilename = launcherPath.split(/[/\\]/).pop() || '';
|
const launcherFilename = launcherPath.split(/[/\\]/).pop() ?? '';
|
||||||
launchers.push({
|
launchers.push({
|
||||||
path: launcherPath,
|
|
||||||
filename: launcherFilename,
|
filename: launcherFilename,
|
||||||
|
path: launcherPath,
|
||||||
size: launcherStats.size,
|
size: launcherStats.size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -55,11 +58,11 @@ export async function getInstalledBackends() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: versionInfo.version,
|
|
||||||
path: launcher.path,
|
|
||||||
filename: launcher.filename,
|
|
||||||
size: launcher.size,
|
|
||||||
actualVersion: versionInfo.actualVersion,
|
actualVersion: versionInfo.actualVersion,
|
||||||
|
filename: launcher.filename,
|
||||||
|
path: launcher.path,
|
||||||
|
size: launcher.size,
|
||||||
|
version: versionInfo.version,
|
||||||
} as InstalledBackend;
|
} as InstalledBackend;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`Could not detect version for ${launcher.filename}:`, error as Error);
|
logError(`Could not detect version for ${launcher.filename}:`, error as Error);
|
||||||
|
|
@ -107,8 +110,8 @@ export async function getCurrentBinaryInfo() {
|
||||||
const filename = pathParts[pathParts.length - 2] || currentBackend.filename;
|
const filename = pathParts[pathParts.length - 2] || currentBackend.filename;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: currentBackend.path,
|
|
||||||
filename,
|
filename,
|
||||||
|
path: currentBackend.path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,21 +133,21 @@ export async function setCurrentBackend(binaryPath: string) {
|
||||||
export async function deleteRelease(binaryPath: string) {
|
export async function deleteRelease(binaryPath: string) {
|
||||||
try {
|
try {
|
||||||
if (!(await pathExists(binaryPath))) {
|
if (!(await pathExists(binaryPath))) {
|
||||||
return { success: false, error: 'Release not found' };
|
return { error: 'Release not found', success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBinaryPath = getCurrentKoboldBinary();
|
const currentBinaryPath = getCurrentKoboldBinary();
|
||||||
if (currentBinaryPath === binaryPath) {
|
if (currentBinaryPath === binaryPath) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
error: 'Cannot delete the currently active release',
|
error: 'Cannot delete the currently active release',
|
||||||
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/');
|
const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/');
|
||||||
|
|
||||||
if (await pathExists(releaseDir)) {
|
if (await pathExists(releaseDir)) {
|
||||||
await rm(releaseDir, { recursive: true, force: true });
|
await rm(releaseDir, { force: true, recursive: true });
|
||||||
|
|
||||||
clearBackendVersionCache(binaryPath);
|
clearBackendVersionCache(binaryPath);
|
||||||
sendToRenderer('versions-updated');
|
sendToRenderer('versions-updated');
|
||||||
|
|
@ -152,9 +155,9 @@ export async function deleteRelease(binaryPath: string) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Release directory not found' };
|
return { error: 'Release directory not found', success: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: (error as Error).message };
|
return { error: (error as Error).message, success: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +177,7 @@ export async function getVersionFromBinary(launcherPath: string) {
|
||||||
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
|
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
|
||||||
if (folderName) {
|
if (folderName) {
|
||||||
const versionMatch = folderName.match(
|
const versionMatch = folderName.match(
|
||||||
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/,
|
||||||
);
|
);
|
||||||
if (versionMatch) {
|
if (versionMatch) {
|
||||||
folderVersion = versionMatch[1];
|
folderVersion = versionMatch[1];
|
||||||
|
|
@ -183,8 +186,8 @@ export async function getVersionFromBinary(launcherPath: string) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await execa(launcherPath, ['--version'], {
|
const result = await execa(launcherPath, ['--version'], {
|
||||||
timeout: 30000,
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const allOutput = (result.stdout + result.stderr).trim();
|
const allOutput = (result.stdout + result.stderr).trim();
|
||||||
|
|
@ -193,7 +196,7 @@ export async function getVersionFromBinary(launcherPath: string) {
|
||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
const lastLine = lines[lines.length - 1].trim();
|
const lastLine = lines[lines.length - 1].trim();
|
||||||
const versionMatch = lastLine.match(
|
const versionMatch = lastLine.match(
|
||||||
/^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
/^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/,
|
||||||
);
|
);
|
||||||
if (versionMatch) {
|
if (versionMatch) {
|
||||||
actualVersion = versionMatch[1];
|
actualVersion = versionMatch[1];
|
||||||
|
|
@ -202,11 +205,11 @@ export async function getVersionFromBinary(launcherPath: string) {
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
version: folderVersion || actualVersion || 'unknown',
|
|
||||||
actualVersion:
|
actualVersion:
|
||||||
folderVersion && actualVersion && folderVersion !== actualVersion
|
folderVersion && actualVersion && folderVersion !== actualVersion
|
||||||
? actualVersion
|
? actualVersion
|
||||||
: undefined,
|
: undefined,
|
||||||
|
version: folderVersion ?? actualVersion ?? 'unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
backendVersionCache.set(launcherPath, result);
|
backendVersionCache.set(launcherPath, result);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { readdir, stat, unlink } from 'node:fs/promises';
|
import { readdir, stat, unlink } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type { KoboldConfig } from '@/types/electron';
|
import type { KoboldConfig } from '@/types/electron';
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||||
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { getInstallDir, setInstallDir } from '../config';
|
import { getInstallDir, setInstallDir } from '../config';
|
||||||
import { getMainWindow, sendToRenderer } from '../window';
|
import { getMainWindow, sendToRenderer } from '../window';
|
||||||
|
|
||||||
|
|
@ -36,7 +39,7 @@ export async function getConfigFiles() {
|
||||||
logError('Error scanning for config files:', error as Error);
|
logError('Error scanning for config files:', error as Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
|
return configFiles.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseConfigFile(filePath: string) {
|
export async function parseConfigFile(filePath: string) {
|
||||||
|
|
@ -74,7 +77,6 @@ export async function selectModelFile(title = 'Select Model File') {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
title,
|
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Model Files',
|
name: 'Model Files',
|
||||||
|
|
@ -83,6 +85,7 @@ export async function selectModelFile(title = 'Select Model File') {
|
||||||
{ name: 'All Files', extensions: ['*'] },
|
{ name: 'All Files', extensions: ['*'] },
|
||||||
],
|
],
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
|
title,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.canceled || result.filePaths.length === 0) {
|
if (result.canceled || result.filePaths.length === 0) {
|
||||||
|
|
@ -95,10 +98,10 @@ export async function selectModelFile(title = 'Select Model File') {
|
||||||
|
|
||||||
export async function selectInstallDirectory() {
|
export async function selectInstallDirectory() {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
|
buttonLabel: 'Select Directory',
|
||||||
|
defaultPath: getInstallDir(),
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
title: `Select the ${PRODUCT_NAME} Installation Directory`,
|
title: `Select the ${PRODUCT_NAME} Installation Directory`,
|
||||||
defaultPath: getInstallDir(),
|
|
||||||
buttonLabel: 'Select Directory',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,23 @@ import { createWriteStream } from 'node:fs';
|
||||||
import { chmod, copyFile, mkdir, rename, rm, unlink } from 'node:fs/promises';
|
import { chmod, copyFile, mkdir, rename, rm, unlink } from 'node:fs/promises';
|
||||||
import { basename, join } from 'node:path';
|
import { basename, join } from 'node:path';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
|
|
||||||
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
|
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
import { getLauncherPath } from '@/utils/node/path';
|
import { getLauncherPath } from '@/utils/node/path';
|
||||||
import { stripAssetExtensions } from '@/utils/version';
|
import { stripAssetExtensions } from '@/utils/version';
|
||||||
|
|
||||||
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
|
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
|
||||||
import { getMainWindow, sendToRenderer } from '../window';
|
import { getMainWindow, sendToRenderer } from '../window';
|
||||||
import { clearBackendVersionCache, getVersionFromBinary } from './backend';
|
import { clearBackendVersionCache, getVersionFromBinary } from './backend';
|
||||||
|
|
||||||
async function removeDirectoryWithRetry(dirPath: string, maxRetries = 3, currentRetry = 0) {
|
async function removeDirectoryWithRetry(dirPath: string, maxRetries = 3, currentRetry = 0) {
|
||||||
try {
|
try {
|
||||||
await rm(dirPath, { recursive: true, force: true });
|
await rm(dirPath, { force: true, recursive: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentRetry < maxRetries) {
|
if (currentRetry < maxRetries) {
|
||||||
const delay = 2 ** currentRetry * 1000;
|
const delay = 2 ** currentRetry * 1000;
|
||||||
|
|
@ -39,7 +42,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
|
||||||
throw new Error(`Failed to download: ${response.statusText}`);
|
throw new Error(`Failed to download: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBytes = parseInt(response.headers.get('content-length') || '0', 10);
|
const totalBytes = parseInt(response.headers.get('content-length') ?? '0', 10);
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
|
|
||||||
const pump = async () => {
|
const pump = async () => {
|
||||||
|
|
@ -81,7 +84,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
})()
|
})(),
|
||||||
);
|
);
|
||||||
writer.on('error', reject);
|
writer.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
@ -122,8 +125,8 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
|
||||||
try {
|
try {
|
||||||
await mkdir(unpackDir, { recursive: true });
|
await mkdir(unpackDir, { recursive: true });
|
||||||
await execa(packedPath, ['--unpack', unpackDir], {
|
await execa(packedPath, ['--unpack', unpackDir], {
|
||||||
timeout: 60000,
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const execaError = error as {
|
const execaError = error as {
|
||||||
|
|
@ -131,8 +134,8 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
|
||||||
stdout?: string;
|
stdout?: string;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
const errorMessage = execaError.stderr || execaError.stdout || execaError.message;
|
const errorMessage = execaError.stderr ?? execaError.stdout ?? execaError.message;
|
||||||
throw new Error(`Unpack failed: ${errorMessage}`);
|
throw new Error(`Unpack failed: ${errorMessage}`, { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,21 +202,20 @@ export async function downloadRelease(asset: GitHubAsset, options: DownloadRelea
|
||||||
await downloadFile(asset, tempPackedFilePath);
|
await downloadFile(asset, tempPackedFilePath);
|
||||||
|
|
||||||
await installBackend({
|
await installBackend({
|
||||||
|
isUpdate: options.isUpdate,
|
||||||
|
oldBackendPath: options.oldBackendPath,
|
||||||
packedFilePath: tempPackedFilePath,
|
packedFilePath: tempPackedFilePath,
|
||||||
unpackedDirPath,
|
unpackedDirPath,
|
||||||
isUpdate: options.isUpdate,
|
|
||||||
wasCurrentBinary: options.wasCurrentBinary,
|
wasCurrentBinary: options.wasCurrentBinary,
|
||||||
oldBackendPath: options.oldBackendPath,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to download or unpack binary:', error as Error);
|
logError('Failed to download or unpack binary:', error as Error);
|
||||||
throw new Error('Failed to download or unpack binary');
|
throw new Error('Failed to download or unpack binary', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importLocalBackend() {
|
export async function importLocalBackend() {
|
||||||
const result = await dialog.showOpenDialog(getMainWindow(), {
|
const result = await dialog.showOpenDialog(getMainWindow(), {
|
||||||
title: 'Select Backend Executable',
|
|
||||||
filters:
|
filters:
|
||||||
platform === 'win32'
|
platform === 'win32'
|
||||||
? [
|
? [
|
||||||
|
|
@ -222,6 +224,7 @@ export async function importLocalBackend() {
|
||||||
]
|
]
|
||||||
: [{ name: 'All Files', extensions: ['*'] }],
|
: [{ name: 'All Files', extensions: ['*'] }],
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
|
title: 'Select Backend Executable',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.canceled || result.filePaths.length === 0) {
|
if (result.canceled || result.filePaths.length === 0) {
|
||||||
|
|
@ -239,12 +242,12 @@ export async function importLocalBackend() {
|
||||||
|
|
||||||
if (!backendVersion || backendVersion.version === 'unknown') {
|
if (!backendVersion || backendVersion.version === 'unknown') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
error: 'Invalid backend executable. Could not determine version information.',
|
error: 'Invalid backend executable. Could not determine version information.',
|
||||||
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = backendVersion.actualVersion || backendVersion.version;
|
const version = backendVersion.actualVersion ?? backendVersion.version;
|
||||||
const filename = basename(selectedPath);
|
const filename = basename(selectedPath);
|
||||||
const baseFilename = stripAssetExtensions(filename);
|
const baseFilename = stripAssetExtensions(filename);
|
||||||
const folderName = `${baseFilename}-${version}`;
|
const folderName = `${baseFilename}-${version}`;
|
||||||
|
|
@ -255,13 +258,13 @@ export async function importLocalBackend() {
|
||||||
|
|
||||||
await installBackend({
|
await installBackend({
|
||||||
packedFilePath,
|
packedFilePath,
|
||||||
unpackedDirPath: installDir,
|
|
||||||
skipUnpackError: true,
|
skipUnpackError: true,
|
||||||
|
unpackedDirPath: installDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to import local backend:', error as Error);
|
logError('Failed to import local backend:', error as Error);
|
||||||
return { success: false, error: (error as Error).message };
|
return { error: (error as Error).message, success: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { type ChildProcess, spawn } from 'node:child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { SERVER_READY_SIGNALS } from '@/constants';
|
import { SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { get as getConfig, getCurrentKoboldBinary, getInstallDir } from '@/main/modules/config';
|
import { get as getConfig, getCurrentKoboldBinary, getInstallDir } from '@/main/modules/config';
|
||||||
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||||
|
|
@ -15,6 +17,7 @@ import { pathExists } from '@/utils/node/fs';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { logError, safeExecute } from '@/utils/node/logging';
|
import { logError, safeExecute } from '@/utils/node/logging';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
|
|
||||||
import { getCurrentBackend } from '../backend';
|
import { getCurrentBackend } from '../backend';
|
||||||
import { abortActiveDownloads, resolveModelPath } from '../model-download';
|
import { abortActiveDownloads, resolveModelPath } from '../model-download';
|
||||||
import { startProxy, stopProxy } from '../proxy';
|
import { startProxy, stopProxy } from '../proxy';
|
||||||
|
|
@ -32,15 +35,17 @@ function spawnPreLaunchCommands(commands: string[]) {
|
||||||
const shellFlag = platform === 'win32' ? '/c' : '-c';
|
const shellFlag = platform === 'win32' ? '/c' : '-c';
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
if (!command.trim()) continue;
|
if (!command.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
sendKoboldOutput(`Running: ${command}\n`);
|
sendKoboldOutput(`Running: ${command}\n`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const child = spawn(shell, [shellFlag, command], {
|
const child = spawn(shell, [shellFlag, command], {
|
||||||
cwd: installDir,
|
cwd: installDir,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
detached: false,
|
detached: false,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
|
|
||||||
preLaunchProcesses.add(child);
|
preLaunchProcesses.add(child);
|
||||||
|
|
@ -68,14 +73,14 @@ function spawnPreLaunchCommands(commands: string[]) {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Failed to start "${command}": ${error instanceof Error ? error.message : String(error)}\n`
|
`Failed to start "${command}": ${error instanceof Error ? error.message : String(error)}\n`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopPreLaunchProcesses() {
|
async function stopPreLaunchProcesses() {
|
||||||
const terminations = Array.from(preLaunchProcesses).map((process) => terminateProcess(process));
|
const terminations = [...preLaunchProcesses].map((process) => terminateProcess(process));
|
||||||
|
|
||||||
await Promise.all(terminations);
|
await Promise.all(terminations);
|
||||||
preLaunchProcesses.clear();
|
preLaunchProcesses.clear();
|
||||||
|
|
@ -83,7 +88,7 @@ async function stopPreLaunchProcesses() {
|
||||||
|
|
||||||
async function resolveModelPaths(args: string[]) {
|
async function resolveModelPaths(args: string[]) {
|
||||||
const resolvedArgs: string[] = [];
|
const resolvedArgs: string[] = [];
|
||||||
const modelParams = [
|
const modelParams = new Set([
|
||||||
'--model',
|
'--model',
|
||||||
'--sdmodel',
|
'--sdmodel',
|
||||||
'--sdt5xxl',
|
'--sdt5xxl',
|
||||||
|
|
@ -98,12 +103,12 @@ async function resolveModelPaths(args: string[]) {
|
||||||
'--ttsmodel',
|
'--ttsmodel',
|
||||||
'--ttswavtokenizer',
|
'--ttswavtokenizer',
|
||||||
'--embeddingsmodel',
|
'--embeddingsmodel',
|
||||||
];
|
]);
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
||||||
if (modelParams.includes(arg) && i + 1 < args.length) {
|
if (modelParams.has(arg) && i + 1 < args.length) {
|
||||||
resolvedArgs.push(arg);
|
resolvedArgs.push(arg);
|
||||||
const urlOrPath = args[i + 1];
|
const urlOrPath = args[i + 1];
|
||||||
try {
|
try {
|
||||||
|
|
@ -132,7 +137,7 @@ export async function launchKoboldCpp(
|
||||||
args: string[] = [],
|
args: string[] = [],
|
||||||
frontendPreference: FrontendPreference = 'koboldcpp',
|
frontendPreference: FrontendPreference = 'koboldcpp',
|
||||||
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference,
|
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference,
|
||||||
preLaunchCommands: string[] = []
|
preLaunchCommands: string[] = [],
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (koboldProcess) {
|
if (koboldProcess) {
|
||||||
|
|
@ -154,12 +159,12 @@ export async function launchKoboldCpp(
|
||||||
: 'No backend configured';
|
: 'No backend configured';
|
||||||
|
|
||||||
logError(
|
logError(
|
||||||
`Launch failed: ${error}. Raw config path: "${rawPath}", Current backend: ${JSON.stringify(currentBackend)}`
|
`Launch failed: ${error}. Raw config path: "${rawPath}", Current backend: ${JSON.stringify(currentBackend)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
error,
|
error,
|
||||||
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,8 +201,8 @@ export async function launchKoboldCpp(
|
||||||
|
|
||||||
const child = spawn(currentBackend.path, finalArgs, {
|
const child = spawn(currentBackend.path, finalArgs, {
|
||||||
cwd: binaryDir,
|
cwd: binaryDir,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
detached: false,
|
detached: false,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
|
|
||||||
koboldProcess = child;
|
koboldProcess = child;
|
||||||
|
|
@ -234,7 +239,7 @@ export async function launchKoboldCpp(
|
||||||
sendToRenderer('server-ready');
|
sendToRenderer('server-ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
readyResolve?.({ success: true, pid: child.pid });
|
readyResolve?.({ pid: child.pid, success: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOutput = (data: Buffer) => {
|
const handleOutput = (data: Buffer) => {
|
||||||
|
|
@ -281,7 +286,7 @@ export async function launchKoboldCpp(
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
readyReject?.(
|
readyReject?.(
|
||||||
new Error(`Process exited before ready signal (code: ${code}, signal: ${signal})`)
|
new Error(`Process exited before ready signal (code: ${code}, signal: ${signal})`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -294,9 +299,9 @@ export async function launchKoboldCpp(
|
||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
const crashInfo: KoboldCrashInfo = {
|
const crashInfo: KoboldCrashInfo = {
|
||||||
|
errorMessage: error.message,
|
||||||
exitCode: null,
|
exitCode: null,
|
||||||
signal: null,
|
signal: null,
|
||||||
errorMessage: error.message,
|
|
||||||
};
|
};
|
||||||
sendToRenderer('kobold-crashed', crashInfo);
|
sendToRenderer('kobold-crashed', crashInfo);
|
||||||
}
|
}
|
||||||
|
|
@ -306,18 +311,18 @@ export async function launchKoboldCpp(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return readyPromise;
|
return await readyPromise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = (error as Error).message;
|
const errorMessage = (error as Error).message;
|
||||||
logError(`Failed to launch: ${errorMessage}`, error as Error);
|
logError(`Failed to launch: ${errorMessage}`, error as Error);
|
||||||
return { success: false, error: errorMessage };
|
return { error: errorMessage, success: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopKoboldCpp() {
|
export async function stopKoboldCpp() {
|
||||||
abortActiveDownloads();
|
abortActiveDownloads();
|
||||||
void stopProxy();
|
void stopProxy();
|
||||||
void stopTunnel();
|
stopTunnel();
|
||||||
void stopPreLaunchProcesses();
|
void stopPreLaunchProcesses();
|
||||||
isIntentionalStop = true;
|
isIntentionalStop = true;
|
||||||
return terminateProcess(koboldProcess);
|
return terminateProcess(koboldProcess);
|
||||||
|
|
@ -325,7 +330,7 @@ export async function stopKoboldCpp() {
|
||||||
|
|
||||||
export const launchKoboldCppWithCustomFrontends = async (
|
export const launchKoboldCppWithCustomFrontends = async (
|
||||||
args: string[] = [],
|
args: string[] = [],
|
||||||
preLaunchCommands: string[] = []
|
preLaunchCommands: string[] = [],
|
||||||
) =>
|
) =>
|
||||||
safeExecute(async () => {
|
safeExecute(async () => {
|
||||||
const frontendPreference = getConfig('frontendPreference');
|
const frontendPreference = getConfig('frontendPreference');
|
||||||
|
|
@ -337,7 +342,7 @@ export const launchKoboldCppWithCustomFrontends = async (
|
||||||
args,
|
args,
|
||||||
frontendPreference,
|
frontendPreference,
|
||||||
imageGenerationFrontendPreference,
|
imageGenerationFrontendPreference,
|
||||||
preLaunchCommands
|
preLaunchCommands,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -351,14 +356,14 @@ export const launchKoboldCppWithCustomFrontends = async (
|
||||||
startSillyTavernFrontend(args).catch((error) => {
|
startSillyTavernFrontend(args).catch((error) => {
|
||||||
logError('Failed to start SillyTavern frontend:', error);
|
logError('Failed to start SillyTavern frontend:', error);
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`
|
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (frontendPreference === 'openwebui') {
|
} else if (frontendPreference === 'openwebui') {
|
||||||
startOpenWebUIFrontend(args).catch((error) => {
|
startOpenWebUIFrontend(args).catch((error) => {
|
||||||
logError('Failed to start OpenWebUI frontend:', error);
|
logError('Failed to start OpenWebUI frontend:', error);
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Failed to start OpenWebUI: ${error instanceof Error ? error.message : String(error)}`
|
`Failed to start OpenWebUI: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { copyFile, readFile, writeFile } from 'node:fs/promises';
|
import { copyFile, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
import { tryExecute } from '@/utils/node/logging';
|
import { tryExecute } from '@/utils/node/logging';
|
||||||
import { getAssetPath } from '@/utils/node/path';
|
import { getAssetPath } from '@/utils/node/path';
|
||||||
|
|
@ -81,7 +82,7 @@ export const patchKliteEmbd = (unpackedDir: string) =>
|
||||||
if (content.includes('gerbil-css-override')) {
|
if (content.includes('gerbil-css-override')) {
|
||||||
patchedContent = patchedContent.replace(
|
patchedContent = patchedContent.replace(
|
||||||
/<style id="gerbil-css-override">[\s\S]*?<\/style>\s*/g,
|
/<style id="gerbil-css-override">[\s\S]*?<\/style>\s*/g,
|
||||||
''
|
'',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { IncomingMessage } from 'node:http';
|
||||||
import { get as httpGet } from 'node:http';
|
import { get as httpGet } from 'node:http';
|
||||||
import { get as httpsGet } from 'node:https';
|
import { get as httpsGet } from 'node:https';
|
||||||
import { basename, dirname, join } from 'node:path';
|
import { basename, dirname, join } from 'node:path';
|
||||||
|
|
||||||
import { getInstallDir } from '@/main/modules/config';
|
import { getInstallDir } from '@/main/modules/config';
|
||||||
import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
|
import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
|
||||||
import type { CachedModel, ModelParamType } from '@/types';
|
import type { CachedModel, ModelParamType } from '@/types';
|
||||||
|
|
@ -34,15 +35,15 @@ function parseHuggingFaceUrl(url: string) {
|
||||||
const pathWithoutQuery = pathWithQuery.split('?')[0];
|
const pathWithoutQuery = pathWithQuery.split('?')[0];
|
||||||
return {
|
return {
|
||||||
author: hfMatch[1],
|
author: hfMatch[1],
|
||||||
model: hfMatch[2],
|
|
||||||
filename: basename(pathWithoutQuery),
|
filename: basename(pathWithoutQuery),
|
||||||
|
model: hfMatch[2],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author: 'external',
|
author: 'external',
|
||||||
model: 'models',
|
|
||||||
filename: basename(url.split('?')[0]),
|
filename: basename(url.split('?')[0]),
|
||||||
|
model: 'models',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@ function normalizeUrl(url: string) {
|
||||||
async function downloadFile(
|
async function downloadFile(
|
||||||
url: string,
|
url: string,
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
onProgress: (progress: DownloadProgress) => void
|
onProgress: (progress: DownloadProgress) => void,
|
||||||
) {
|
) {
|
||||||
const normalizedUrl = normalizeUrl(url);
|
const normalizedUrl = normalizeUrl(url);
|
||||||
const outputDir = dirname(outputPath);
|
const outputDir = dirname(outputPath);
|
||||||
|
|
@ -78,11 +79,11 @@ async function downloadFile(
|
||||||
let isAborted = false;
|
let isAborted = false;
|
||||||
|
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((done) => {
|
||||||
if (fileStream) {
|
if (fileStream) {
|
||||||
fileStream.close(() => resolve());
|
fileStream.close(() => done());
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await unlink(tempPath).catch(() => void 0);
|
await unlink(tempPath).catch(() => void 0);
|
||||||
|
|
@ -101,7 +102,9 @@ async function downloadFile(
|
||||||
activeDownloads.add(abortController);
|
activeDownloads.add(abortController);
|
||||||
|
|
||||||
const handleRedirect = (requestUrl: string, redirectCount = 0): void => {
|
const handleRedirect = (requestUrl: string, redirectCount = 0): void => {
|
||||||
if (isAborted) return;
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (redirectCount > 10) {
|
if (redirectCount > 10) {
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
|
|
@ -110,7 +113,9 @@ async function downloadFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
httpModule(requestUrl, (response) => {
|
httpModule(requestUrl, (response) => {
|
||||||
if (isAborted) return;
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentRequest = response;
|
currentRequest = response;
|
||||||
if (
|
if (
|
||||||
response.statusCode === 301 ||
|
response.statusCode === 301 ||
|
||||||
|
|
@ -133,7 +138,7 @@ async function downloadFile(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
const totalBytes = parseInt(response.headers['content-length'] ?? '0', 10);
|
||||||
let downloadedBytes = 0;
|
let downloadedBytes = 0;
|
||||||
let lastReportTime = Date.now();
|
let lastReportTime = Date.now();
|
||||||
let lastReportedBytes = 0;
|
let lastReportedBytes = 0;
|
||||||
|
|
@ -169,12 +174,12 @@ async function downloadFile(
|
||||||
sendToRenderer('kobold-output', `\r${progressMsg}`);
|
sendToRenderer('kobold-output', `\r${progressMsg}`);
|
||||||
|
|
||||||
onProgress({
|
onProgress({
|
||||||
type: 'progress',
|
|
||||||
percent,
|
|
||||||
downloaded: `${downloadedMB}MB`,
|
downloaded: `${downloadedMB}MB`,
|
||||||
total: totalBytes ? `${totalMB}MB` : undefined,
|
|
||||||
speed: `${speedMBPerSec}MB/s`,
|
|
||||||
eta: totalBytes ? etaStr : undefined,
|
eta: totalBytes ? etaStr : undefined,
|
||||||
|
percent,
|
||||||
|
speed: `${speedMBPerSec}MB/s`,
|
||||||
|
total: totalBytes ? `${totalMB}MB` : undefined,
|
||||||
|
type: 'progress',
|
||||||
});
|
});
|
||||||
|
|
||||||
lastReportTime = now;
|
lastReportTime = now;
|
||||||
|
|
@ -183,15 +188,17 @@ async function downloadFile(
|
||||||
});
|
});
|
||||||
|
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
if (isAborted) return;
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (totalBytes > 0 && downloadedBytes !== totalBytes) {
|
if (totalBytes > 0 && downloadedBytes !== totalBytes) {
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
void cleanup();
|
void cleanup();
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`Incomplete download: received ${downloadedBytes} bytes, expected ${totalBytes} bytes`
|
`Incomplete download: received ${downloadedBytes} bytes, expected ${totalBytes} bytes`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -206,11 +213,11 @@ async function downloadFile(
|
||||||
sendKoboldOutput('\n');
|
sendKoboldOutput('\n');
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
reject(err instanceof Error ? err : new Error(String(err)));
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
}
|
}
|
||||||
})()
|
})(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -218,25 +225,31 @@ async function downloadFile(
|
||||||
'error',
|
'error',
|
||||||
(err) =>
|
(err) =>
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (isAborted) return;
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
await cleanup();
|
await cleanup();
|
||||||
reject(err);
|
reject(err);
|
||||||
})()
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
fileStream.on(
|
fileStream.on(
|
||||||
'error',
|
'error',
|
||||||
(err) =>
|
(err) =>
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (isAborted) return;
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
await cleanup();
|
await cleanup();
|
||||||
reject(err);
|
reject(err);
|
||||||
})()
|
})(),
|
||||||
);
|
);
|
||||||
}).on('error', (err) => {
|
}).on('error', (err) => {
|
||||||
if (isAborted) return;
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
@ -263,7 +276,7 @@ function isValidModelUrl(url: string) {
|
||||||
export async function resolveModelPath(
|
export async function resolveModelPath(
|
||||||
urlOrPath: string,
|
urlOrPath: string,
|
||||||
paramType: ModelParamType,
|
paramType: ModelParamType,
|
||||||
onProgress?: (progress: DownloadProgress) => void
|
onProgress?: (progress: DownloadProgress) => void,
|
||||||
) {
|
) {
|
||||||
if (!isValidModelUrl(urlOrPath)) {
|
if (!isValidModelUrl(urlOrPath)) {
|
||||||
return urlOrPath;
|
return urlOrPath;
|
||||||
|
|
@ -274,32 +287,33 @@ export async function resolveModelPath(
|
||||||
if (await pathExists(localPath)) {
|
if (await pathExists(localPath)) {
|
||||||
sendKoboldOutput(`Using cached model at: ${localPath}`);
|
sendKoboldOutput(`Using cached model at: ${localPath}`);
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
type: 'complete',
|
|
||||||
localPath,
|
localPath,
|
||||||
|
type: 'complete',
|
||||||
});
|
});
|
||||||
return localPath;
|
return localPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendKoboldOutput(`Downloading model from ${urlOrPath} to ${localPath}...`);
|
sendKoboldOutput(`Downloading model from ${urlOrPath} to ${localPath}...`);
|
||||||
|
|
||||||
const progressCallback = onProgress || ((p: DownloadProgress) => p);
|
const progressCallback = onProgress ?? ((p: DownloadProgress) => p);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadFile(urlOrPath, localPath, progressCallback);
|
await downloadFile(urlOrPath, localPath, progressCallback);
|
||||||
|
|
||||||
sendKoboldOutput(`Model downloaded successfully to: ${localPath}\n`);
|
sendKoboldOutput(`Model downloaded successfully to: ${localPath}\n`);
|
||||||
progressCallback({
|
progressCallback({
|
||||||
type: 'complete',
|
|
||||||
localPath,
|
localPath,
|
||||||
|
type: 'complete',
|
||||||
});
|
});
|
||||||
return localPath;
|
return localPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
progressCallback({
|
progressCallback({
|
||||||
type: 'error',
|
|
||||||
error: `Download failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
error: `Download failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
type: 'error',
|
||||||
});
|
});
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to download model from ${urlOrPath}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
`Failed to download model from ${urlOrPath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
{ cause: error },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,9 +351,9 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
|
||||||
|
|
||||||
if (fileStat.isFile()) {
|
if (fileStat.isFile()) {
|
||||||
models.push({
|
models.push({
|
||||||
path: filePath,
|
|
||||||
author,
|
author,
|
||||||
model: modelDir,
|
model: modelDir,
|
||||||
|
path: filePath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +369,7 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function abortActiveDownloads() {
|
export function abortActiveDownloads() {
|
||||||
const downloads = Array.from(activeDownloads);
|
const downloads = [...activeDownloads];
|
||||||
for (const controller of downloads) {
|
for (const controller of downloads) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
|
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { sendKoboldOutput } from '../window';
|
import { sendKoboldOutput } from '../window';
|
||||||
|
|
||||||
let proxyServer: http.Server | null = null;
|
let proxyServer: http.Server | null = null;
|
||||||
|
|
@ -18,11 +20,11 @@ const replaceKoboldWithGerbil = (data: string) => {
|
||||||
|
|
||||||
const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) => {
|
const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: koboldCppHost,
|
|
||||||
port: koboldCppPort,
|
|
||||||
path: clientReq.url,
|
|
||||||
method: clientReq.method,
|
|
||||||
headers: clientReq.headers,
|
headers: clientReq.headers,
|
||||||
|
hostname: koboldCppHost,
|
||||||
|
method: clientReq.method,
|
||||||
|
path: clientReq.url,
|
||||||
|
port: koboldCppPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxyReq = http.request(options, (proxyRes) => {
|
const proxyReq = http.request(options, (proxyRes) => {
|
||||||
|
|
@ -38,14 +40,14 @@ const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) =>
|
||||||
proxyRes.on('end', () => {
|
proxyRes.on('end', () => {
|
||||||
const modifiedBody = replaceKoboldWithGerbil(body);
|
const modifiedBody = replaceKoboldWithGerbil(body);
|
||||||
|
|
||||||
clientRes.writeHead(proxyRes.statusCode || 200, {
|
clientRes.writeHead(proxyRes.statusCode ?? 200, {
|
||||||
...proxyRes.headers,
|
...proxyRes.headers,
|
||||||
'content-length': Buffer.byteLength(modifiedBody),
|
'content-length': Buffer.byteLength(modifiedBody),
|
||||||
});
|
});
|
||||||
clientRes.end(modifiedBody);
|
clientRes.end(modifiedBody);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
clientRes.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
|
||||||
proxyRes.pipe(clientRes);
|
proxyRes.pipe(clientRes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@ import path from 'node:path';
|
||||||
import { arch, platform } from 'node:process';
|
import { arch, platform } from 'node:process';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { execa, type ResultPromise } from 'execa';
|
|
||||||
|
import { execa } from 'execa';
|
||||||
|
import type { ResultPromise } from 'execa';
|
||||||
|
|
||||||
import { GITHUB_API, OPENWEBUI, SILLYTAVERN } from '@/constants';
|
import { GITHUB_API, OPENWEBUI, SILLYTAVERN } from '@/constants';
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
import { getInstallDir } from '@/main/modules/config';
|
import { getInstallDir } from '@/main/modules/config';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { sendKoboldOutput, sendToRenderer } from '../window';
|
import { sendKoboldOutput, sendToRenderer } from '../window';
|
||||||
|
|
||||||
let activeTunnel: ResultPromise | null = null;
|
let activeTunnel: ResultPromise | null = null;
|
||||||
|
|
@ -51,7 +55,6 @@ const downloadCloudflared = async (binPath: string) => {
|
||||||
throw new Error(`Failed to download cloudflared: ${response.statusText}`);
|
throw new Error(`Failed to download cloudflared: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Node.js stream types are incompatible with web streams
|
|
||||||
await pipeline(Readable.fromWeb(response.body as any), createWriteStream(binPath));
|
await pipeline(Readable.fromWeb(response.body as any), createWriteStream(binPath));
|
||||||
|
|
||||||
if (platform !== 'win32') {
|
if (platform !== 'win32') {
|
||||||
|
|
@ -63,16 +66,19 @@ const downloadCloudflared = async (binPath: string) => {
|
||||||
|
|
||||||
const getTunnelTarget = (frontendPreference: FrontendPreference) => {
|
const getTunnelTarget = (frontendPreference: FrontendPreference) => {
|
||||||
switch (frontendPreference) {
|
switch (frontendPreference) {
|
||||||
case 'sillytavern':
|
case 'sillytavern': {
|
||||||
return `http://${SILLYTAVERN.HOST}:${SILLYTAVERN.PROXY_PORT}`;
|
return `http://${SILLYTAVERN.HOST}:${SILLYTAVERN.PROXY_PORT}`;
|
||||||
case 'openwebui':
|
}
|
||||||
|
case 'openwebui': {
|
||||||
return `http://${OPENWEBUI.HOST}:${OPENWEBUI.PORT}`;
|
return `http://${OPENWEBUI.HOST}:${OPENWEBUI.PORT}`;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
return `http://${PROXY.HOST}:${PROXY.PORT}`;
|
return `http://${PROXY.HOST}:${PROXY.PORT}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForBackend = async (url: string, timeoutMs = 30000) => {
|
const waitForBackend = async (url: string, timeoutMs = 30_000) => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
while (Date.now() - startTime < timeoutMs) {
|
while (Date.now() - startTime < timeoutMs) {
|
||||||
|
|
@ -104,7 +110,7 @@ export const startTunnel = async (frontendPreference: FrontendPreference = 'kobo
|
||||||
|
|
||||||
if (!backendReady) {
|
if (!backendReady) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Backend not ready after 30 seconds. Start your backend first before enabling tunnel.'
|
'Backend not ready after 30 seconds. Start your backend first before enabling tunnel.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +153,7 @@ export const startTunnel = async (frontendPreference: FrontendPreference = 'kobo
|
||||||
: 'Tunnel connection timed out';
|
: 'Tunnel connection timed out';
|
||||||
tunnel.kill();
|
tunnel.kill();
|
||||||
reject(new Error(message));
|
reject(new Error(message));
|
||||||
}, 30000);
|
}, 30_000);
|
||||||
|
|
||||||
const checkForUrl = () => {
|
const checkForUrl = () => {
|
||||||
const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import {
|
import {
|
||||||
cpuTemperature,
|
cpuTemperature,
|
||||||
|
|
@ -8,8 +9,10 @@ import {
|
||||||
powerShellRelease,
|
powerShellRelease,
|
||||||
powerShellStart,
|
powerShellStart,
|
||||||
} from 'systeminformation';
|
} from 'systeminformation';
|
||||||
|
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
import { getGPUData } from '@/utils/node/gpu';
|
||||||
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { detectGPU } from './hardware';
|
import { detectGPU } from './hardware';
|
||||||
import { isTrayActive, updateMetrics } from './tray';
|
import { isTrayActive, updateMetrics } from './tray';
|
||||||
|
|
||||||
|
|
@ -65,7 +68,9 @@ let latestMemoryMetrics: MemoryMetrics | null = null;
|
||||||
let latestGpuMetrics: GpuMetrics | null = null;
|
let latestGpuMetrics: GpuMetrics | null = null;
|
||||||
|
|
||||||
export function startMonitoring(window: BrowserWindow) {
|
export function startMonitoring(window: BrowserWindow) {
|
||||||
if (isRunning) return;
|
if (isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mainWindow = window;
|
mainWindow = window;
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
|
|
@ -148,9 +153,9 @@ async function collectAndSendMemoryMetrics() {
|
||||||
const usedBytes = memData.active || memData.used;
|
const usedBytes = memData.active || memData.used;
|
||||||
const totalBytes = memData.total;
|
const totalBytes = memData.total;
|
||||||
const metrics: MemoryMetrics = {
|
const metrics: MemoryMetrics = {
|
||||||
used: usedBytes / (1024 * 1024 * 1024),
|
|
||||||
total: totalBytes / (1024 * 1024 * 1024),
|
total: totalBytes / (1024 * 1024 * 1024),
|
||||||
usage: Math.round((usedBytes / totalBytes) * 100),
|
usage: Math.round((usedBytes / totalBytes) * 100),
|
||||||
|
used: usedBytes / (1024 * 1024 * 1024),
|
||||||
};
|
};
|
||||||
|
|
||||||
latestMemoryMetrics = metrics;
|
latestMemoryMetrics = metrics;
|
||||||
|
|
@ -166,16 +171,13 @@ async function collectAndSendGpuMetrics() {
|
||||||
await tryExecute(async () => {
|
await tryExecute(async () => {
|
||||||
const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]);
|
const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]);
|
||||||
const metrics: GpuMetrics = {
|
const metrics: GpuMetrics = {
|
||||||
gpus: gpuData.map((gpuData, index) => ({
|
gpus: gpuData.map((gpu, index) => ({
|
||||||
|
memoryTotal: gpu.memoryTotal,
|
||||||
|
memoryUsage: gpu.memoryTotal > 0 ? Math.round((gpu.memoryUsed / gpu.memoryTotal) * 100) : 0,
|
||||||
|
memoryUsed: gpu.memoryUsed,
|
||||||
name: gpuInfo.gpuInfo[index] || `GPU ${index}`,
|
name: gpuInfo.gpuInfo[index] || `GPU ${index}`,
|
||||||
usage: Math.round(gpuData.usage),
|
temperature: gpu.temperature,
|
||||||
memoryUsed: gpuData.memoryUsed,
|
usage: Math.round(gpu.usage),
|
||||||
memoryTotal: gpuData.memoryTotal,
|
|
||||||
memoryUsage:
|
|
||||||
gpuData.memoryTotal > 0
|
|
||||||
? Math.round((gpuData.memoryUsed / gpuData.memoryTotal) * 100)
|
|
||||||
: 0,
|
|
||||||
temperature: gpuData.temperature,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -258,17 +260,17 @@ export const openPerformanceManager = async () =>
|
||||||
case 'darwin': {
|
case 'darwin': {
|
||||||
const success = await tryLaunchCommand('open', ['-a', 'Activity Monitor']);
|
const success = await tryLaunchCommand('open', ['-a', 'Activity Monitor']);
|
||||||
if (success) {
|
if (success) {
|
||||||
return { success: true, app: 'Activity Monitor' };
|
return { app: 'Activity Monitor', success: true };
|
||||||
}
|
}
|
||||||
return { success: false, error: 'Could not open Activity Monitor' };
|
return { error: 'Could not open Activity Monitor', success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'win32': {
|
case 'win32': {
|
||||||
const success = await tryLaunchCommand('taskmgr');
|
const success = await tryLaunchCommand('taskmgr');
|
||||||
if (success) {
|
if (success) {
|
||||||
return { success: true, app: 'Task Manager' };
|
return { app: 'Task Manager', success: true };
|
||||||
}
|
}
|
||||||
return { success: false, error: 'Could not open Task Manager' };
|
return { error: 'Could not open Task Manager', success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'linux': {
|
case 'linux': {
|
||||||
|
|
@ -276,7 +278,7 @@ export const openPerformanceManager = async () =>
|
||||||
if (app) {
|
if (app) {
|
||||||
const success = await tryLaunchCommand(app);
|
const success = await tryLaunchCommand(app);
|
||||||
if (success) {
|
if (success) {
|
||||||
return { success: true, app };
|
return { app, success: true };
|
||||||
}
|
}
|
||||||
cachedLinuxApp = null;
|
cachedLinuxApp = null;
|
||||||
linuxAppSearchComplete = false;
|
linuxAppSearchComplete = false;
|
||||||
|
|
@ -284,24 +286,24 @@ export const openPerformanceManager = async () =>
|
||||||
if (fallbackApp) {
|
if (fallbackApp) {
|
||||||
const fallbackSuccess = await tryLaunchCommand(fallbackApp);
|
const fallbackSuccess = await tryLaunchCommand(fallbackApp);
|
||||||
if (fallbackSuccess) {
|
if (fallbackSuccess) {
|
||||||
return { success: true, app: fallbackApp };
|
return { app: fallbackApp, success: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
error: `Could not find any performance monitoring app. Tried: ${LINUX_PERFORMANCE_APPS.join(', ')}`,
|
error: `Could not find any performance monitoring app. Tried: ${LINUX_PERFORMANCE_APPS.join(', ')}`,
|
||||||
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return {
|
return {
|
||||||
success: false,
|
|
||||||
error: `Unsupported platform: ${platform}`,
|
error: `Unsupported platform: ${platform}`,
|
||||||
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 'Failed to open performance manager')) || {
|
}, 'Failed to open performance manager')) ?? {
|
||||||
success: false,
|
|
||||||
error: 'Failed to open performance manager',
|
error: 'Failed to open performance manager',
|
||||||
|
success: false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT } from '@/constants/notepad';
|
import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT } from '@/constants/notepad';
|
||||||
import type { SavedNotepadState } from '@/types/electron';
|
import type { SavedNotepadState } from '@/types/electron';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
|
|
||||||
import { get, getInstallDir, set } from './config';
|
import { get, getInstallDir, set } from './config';
|
||||||
|
|
||||||
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
|
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
|
||||||
activeTabId: null,
|
activeTabId: null,
|
||||||
position: DEFAULT_NOTEPAD_POSITION,
|
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
|
position: DEFAULT_NOTEPAD_POSITION,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNotepadDir = () => join(getInstallDir(), 'notepad');
|
const getNotepadDir = () => join(getInstallDir(), 'notepad');
|
||||||
|
|
@ -50,7 +52,7 @@ export const saveTabContent = async (title: string, content: string) => {
|
||||||
const notepadDir = getNotepadDir();
|
const notepadDir = getNotepadDir();
|
||||||
const filePath = join(notepadDir, `${title}.txt`);
|
const filePath = join(notepadDir, `${title}.txt`);
|
||||||
|
|
||||||
await writeFile(filePath, content, 'utf-8');
|
await writeFile(filePath, content, 'utf8');
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -60,7 +62,7 @@ export const saveTabContent = async (title: string, content: string) => {
|
||||||
export const loadTabContent = async (title: string) => {
|
export const loadTabContent = async (title: string) => {
|
||||||
try {
|
try {
|
||||||
const notepadDir = getNotepadDir();
|
const notepadDir = getNotepadDir();
|
||||||
return readFile(join(notepadDir, `${title}.txt`), 'utf-8');
|
return await readFile(join(notepadDir, `${title}.txt`), 'utf8');
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -72,14 +74,14 @@ export const saveNotepadState = async (state: SavedNotepadState) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadNotepadState = async () => {
|
export const loadNotepadState = async () => {
|
||||||
const stored = get('notepad') || DEFAULT_NOTEPAD_STATE;
|
const stored = get('notepad') ?? DEFAULT_NOTEPAD_STATE;
|
||||||
const tabs = await getTabsFromStorage();
|
const tabs = await getTabsFromStorage();
|
||||||
const activeTabId =
|
const activeTabId =
|
||||||
stored.activeTabId && tabs.some((tab) => tab.title === stored.activeTabId)
|
stored.activeTabId && tabs.some((tab) => tab.title === stored.activeTabId)
|
||||||
? stored.activeTabId
|
? stored.activeTabId
|
||||||
: tabs[0]?.title || null;
|
: tabs[0]?.title || null;
|
||||||
|
|
||||||
return { ...stored, tabs, activeTabId };
|
return { ...stored, activeTabId, tabs };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTabFile = async (title: string) => {
|
export const deleteTabFile = async (title: string) => {
|
||||||
|
|
@ -98,16 +100,19 @@ export const createNewTab = async (title?: string) => {
|
||||||
.map((tab) => tab.title.match(/^Note (\d+)$/)?.[1])
|
.map((tab) => tab.title.match(/^Note (\d+)$/)?.[1])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(Number)
|
.map(Number)
|
||||||
.sort((a, b) => a - b);
|
.toSorted((a, b) => a - b);
|
||||||
|
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
for (const num of noteNumbers) {
|
for (const num of noteNumbers) {
|
||||||
if (num === counter) counter++;
|
if (num === counter) {
|
||||||
else break;
|
counter++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
title = `Note ${counter}`;
|
title = `Note ${counter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveTabContent(title, DEFAULT_TAB_CONTENT);
|
await saveTabContent(title, DEFAULT_TAB_CONTENT);
|
||||||
return { title, content: DEFAULT_TAB_CONTENT };
|
return { content: DEFAULT_TAB_CONTENT, title };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import { spawn } from 'node:child_process';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { logError } from '@/utils/node/logging';
|
import { logError } from '@/utils/node/logging';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
|
|
||||||
import { getInstallDir } from './config';
|
import { getInstallDir } from './config';
|
||||||
import { getUvEnvironment } from './dependencies';
|
import { getUvEnvironment } from './dependencies';
|
||||||
import { sendKoboldOutput, sendToRenderer } from './window';
|
import { sendKoboldOutput, sendToRenderer } from './window';
|
||||||
|
|
@ -30,9 +32,9 @@ async function createUvProcess(args: string[], env?: Record<string, string>) {
|
||||||
const mergedEnv = { ...uvEnv, ...env };
|
const mergedEnv = { ...uvEnv, ...env };
|
||||||
|
|
||||||
return spawn('uvx', args, {
|
return spawn('uvx', args, {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
detached: false,
|
detached: false,
|
||||||
env: mergedEnv,
|
env: mergedEnv,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +43,9 @@ async function waitForOpenWebUIToStart() {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
|
||||||
const checkForOutput = (data: Buffer) => {
|
const checkForOutput = (data: Buffer) => {
|
||||||
if (resolved) return;
|
if (resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
|
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
|
|
@ -76,7 +80,7 @@ export async function startFrontend(args: string[]) {
|
||||||
const appVersion = app.getVersion();
|
const appVersion = app.getVersion();
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Preparing Open WebUI to connect at ${koboldHost}:${koboldPort}${isImageMode ? ' (with image generation)' : ''}...`
|
`Preparing Open WebUI to connect at ${koboldHost}:${koboldPort}${isImageMode ? ' (with image generation)' : ''}...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||||
|
|
@ -85,21 +89,21 @@ export async function startFrontend(args: string[]) {
|
||||||
const openWebUIArgs = [...OPENWEBUI_BASE_ARGS, '--port', config.port.toString()];
|
const openWebUIArgs = [...OPENWEBUI_BASE_ARGS, '--port', config.port.toString()];
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Starting Open WebUI with uv${isImageMode ? ' (image generation enabled)' : ''}...`
|
`Starting Open WebUI with uv${isImageMode ? ' (image generation enabled)' : ''}...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const installDir = getInstallDir();
|
const installDir = getInstallDir();
|
||||||
const openWebUIDataDir = join(installDir, 'openwebui-data');
|
const openWebUIDataDir = join(installDir, 'openwebui-data');
|
||||||
|
|
||||||
const envConfig: Record<string, string> = {
|
const envConfig: Record<string, string> = {
|
||||||
OPENAI_API_BASE_URL: `${proxyUrl}/v1`,
|
|
||||||
DATA_DIR: openWebUIDataDir,
|
DATA_DIR: openWebUIDataDir,
|
||||||
|
ENABLE_OLLAMA_API: 'false',
|
||||||
|
ENABLE_VERSION_UPDATE_CHECK: 'false',
|
||||||
|
GLOBAL_LOG_LEVEL: 'warning',
|
||||||
|
OPENAI_API_BASE_URL: `${proxyUrl}/v1`,
|
||||||
|
USER_AGENT: `Gerbil/${appVersion}`,
|
||||||
WEBUI_AUTH: 'false',
|
WEBUI_AUTH: 'false',
|
||||||
WEBUI_SECRET_KEY: 'gerbil',
|
WEBUI_SECRET_KEY: 'gerbil',
|
||||||
ENABLE_OLLAMA_API: 'false',
|
|
||||||
USER_AGENT: `Gerbil/${appVersion}`,
|
|
||||||
GLOBAL_LOG_LEVEL: 'warning',
|
|
||||||
ENABLE_VERSION_UPDATE_CHECK: 'false',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isImageMode) {
|
if (isImageMode) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { safeExecute } from '@/utils/node/logging';
|
import { safeExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
import { detectGPU } from './hardware';
|
import { detectGPU } from './hardware';
|
||||||
|
|
||||||
export interface PlatformGPUInfo {
|
export interface PlatformGPUInfo {
|
||||||
|
|
@ -25,7 +27,7 @@ export async function getPlatformGPUInfo(): Promise<PlatformGPUInfo> {
|
||||||
}, 'Failed to detect platform GPU information');
|
}, 'Failed to detect platform GPU information');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
result || {
|
result ?? {
|
||||||
hasAMD: false,
|
hasAMD: false,
|
||||||
hasNVIDIA: false,
|
hasNVIDIA: false,
|
||||||
isWindows: platform === 'win32',
|
isWindows: platform === 'win32',
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import type { ChildProcess } from 'node:child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { createServer, request, type Server } from 'node:http';
|
import { createServer, request } from 'node:http';
|
||||||
|
import type { Server } from 'node:http';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { platform } from 'node:process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { SERVER_READY_SIGNALS, SILLYTAVERN } from '@/constants';
|
import { SERVER_READY_SIGNALS, SILLYTAVERN } from '@/constants';
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { logError, tryExecute } from '@/utils/node/logging';
|
import { logError, tryExecute } from '@/utils/node/logging';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
|
|
||||||
import { getInstallDir } from './config';
|
import { getInstallDir } from './config';
|
||||||
import { getNodeEnvironment } from './dependencies';
|
import { getNodeEnvironment } from './dependencies';
|
||||||
import { sendKoboldOutput, sendToRenderer } from './window';
|
import { sendKoboldOutput, sendToRenderer } from './window';
|
||||||
|
|
@ -53,11 +56,11 @@ async function ensureSillyTavernInstalled() {
|
||||||
platform === 'win32' ? ['/s', '/q', nodeModulesPath] : ['-rf', nodeModulesPath];
|
platform === 'win32' ? ['/s', '/q', nodeModulesPath] : ['-rf', nodeModulesPath];
|
||||||
|
|
||||||
spawn(rmCmd, rmArgs, {
|
spawn(rmCmd, rmArgs, {
|
||||||
stdio: 'inherit',
|
|
||||||
shell: true,
|
shell: true,
|
||||||
|
stdio: 'inherit',
|
||||||
})
|
})
|
||||||
.on('exit', (code) =>
|
.on('exit', (code) =>
|
||||||
code === 0 ? resolve() : reject(new Error(`Failed with code ${code}`))
|
code === 0 ? resolve() : reject(new Error(`Failed with code ${code}`)),
|
||||||
)
|
)
|
||||||
.on('error', reject);
|
.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
@ -83,10 +86,10 @@ async function ensureSillyTavernInstalled() {
|
||||||
'--silent',
|
'--silent',
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
env,
|
env,
|
||||||
shell: platform === 'win32',
|
shell: platform === 'win32',
|
||||||
}
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let errorOutput = '';
|
let errorOutput = '';
|
||||||
|
|
@ -121,11 +124,11 @@ async function createNpxProcess(args: string[]) {
|
||||||
const installDir = getSillyTavernInstallDir();
|
const installDir = getSillyTavernInstallDir();
|
||||||
|
|
||||||
return spawn('node', [serverJsPath, ...args], {
|
return spawn('node', [serverJsPath, ...args], {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
cwd: installDir,
|
||||||
detached: false,
|
detached: false,
|
||||||
env,
|
env,
|
||||||
cwd: installDir,
|
|
||||||
shell: platform === 'win32',
|
shell: platform === 'win32',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +141,7 @@ async function ensureSillyTavernSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
'SillyTavern settings not found, starting SillyTavern briefly to generate config...'
|
'SillyTavern settings not found, starting SillyTavern briefly to generate config...',
|
||||||
);
|
);
|
||||||
|
|
||||||
const initProcess = await createNpxProcess(SILLYTAVERN_BASE_ARGS);
|
const initProcess = await createNpxProcess(SILLYTAVERN_BASE_ARGS);
|
||||||
|
|
@ -189,7 +192,7 @@ async function ensureSillyTavernSettings() {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
2000
|
2000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -223,14 +226,14 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||||
|
|
||||||
const proxyUrl = PROXY.URL;
|
const proxyUrl = PROXY.URL;
|
||||||
|
|
||||||
if (!settings.power_user) settings.power_user = {};
|
settings.power_user ??= {};
|
||||||
const powerUser = settings.power_user as Record<string, unknown>;
|
const powerUser = settings.power_user as Record<string, unknown>;
|
||||||
powerUser.auto_connect = true;
|
powerUser.auto_connect = true;
|
||||||
|
|
||||||
if (!settings.textgenerationwebui_settings) settings.textgenerationwebui_settings = {};
|
settings.textgenerationwebui_settings ??= {};
|
||||||
const textgenSettings = settings.textgenerationwebui_settings as Record<string, unknown>;
|
const textgenSettings = settings.textgenerationwebui_settings as Record<string, unknown>;
|
||||||
|
|
||||||
if (!textgenSettings.server_urls) textgenSettings.server_urls = {};
|
textgenSettings.server_urls ??= {};
|
||||||
const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
|
const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
|
||||||
serverUrls.koboldcpp = proxyUrl;
|
serverUrls.koboldcpp = proxyUrl;
|
||||||
|
|
||||||
|
|
@ -246,7 +249,7 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||||
`2. Go to 'Extensions' tab and enable 'Image Generation'\n` +
|
`2. Go to 'Extensions' tab and enable 'Image Generation'\n` +
|
||||||
`3. Set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` +
|
`3. Set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` +
|
||||||
`4. Set API URL to: ${proxyUrl}/sdui\n` +
|
`4. Set API URL to: ${proxyUrl}/sdui\n` +
|
||||||
`5. Click 'Connect' to test the connection`
|
`5. Click 'Connect' to test the connection`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,17 +290,17 @@ async function waitForSillyTavernToStart() {
|
||||||
const createProxyServer = (targetPort: number, proxyPort: number) => {
|
const createProxyServer = (targetPort: number, proxyPort: number) => {
|
||||||
proxyServer = createServer((req, res) => {
|
proxyServer = createServer((req, res) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: 'localhost',
|
|
||||||
port: targetPort,
|
|
||||||
path: req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
|
hostname: 'localhost',
|
||||||
|
method: req.method,
|
||||||
|
path: req.url,
|
||||||
|
port: targetPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxyReq = request(options, (proxyRes) => {
|
const proxyReq = request(options, (proxyRes) => {
|
||||||
const headers = { ...proxyRes.headers };
|
const headers = { ...proxyRes.headers };
|
||||||
delete headers['x-frame-options'];
|
delete headers['x-frame-options'];
|
||||||
res.writeHead(proxyRes.statusCode || 200, headers);
|
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
||||||
proxyRes.pipe(res);
|
proxyRes.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -382,7 +385,7 @@ export async function startFrontend(args: string[]) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(
|
logError(
|
||||||
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
error as Error
|
error as Error,
|
||||||
);
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -402,7 +405,7 @@ export async function stopFrontend() {
|
||||||
proxyServer = null;
|
proxyServer = null;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { createServer, type Server } from 'node:http';
|
import { createServer } from 'node:http';
|
||||||
|
import type { Server } from 'node:http';
|
||||||
import { join, normalize, resolve as resolvePath, sep } from 'node:path';
|
import { join, normalize, resolve as resolvePath, sep } from 'node:path';
|
||||||
|
|
||||||
import { lookup } from 'mime-types';
|
import { lookup } from 'mime-types';
|
||||||
|
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
|
|
||||||
let server: Server | null = null;
|
let server: Server | null = null;
|
||||||
|
|
@ -40,7 +43,7 @@ export const startStaticServer = (distPath: string) =>
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end('Not found');
|
res.end('Not found');
|
||||||
}
|
}
|
||||||
})()
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.listen(0, 'localhost', () => {
|
server.listen(0, 'localhost', () => {
|
||||||
|
|
|
||||||
7
src/main/modules/tray-active.ts
Normal file
7
src/main/modules/tray-active.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
let trayActive = false;
|
||||||
|
|
||||||
|
export const setTrayActive = (active: boolean) => {
|
||||||
|
trayActive = active;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isTrayActive = () => trayActive;
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { platform, resourcesPath } from 'node:process';
|
import { platform, resourcesPath } from 'node:process';
|
||||||
import { app, Menu, type MenuItemConstructorOptions, nativeImage, Tray } from 'electron';
|
|
||||||
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
import { app, Menu, nativeImage, Tray } from 'electron';
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type { Screen } from '@/types';
|
import type { Screen } from '@/types';
|
||||||
import { stripFileExtension } from '@/utils/format';
|
import { stripFileExtension } from '@/utils/format';
|
||||||
|
|
||||||
import { getEnableSystemTray } from './config';
|
import { getEnableSystemTray } from './config';
|
||||||
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from './monitoring';
|
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from './monitoring';
|
||||||
|
import { setTrayActive } from './tray-active';
|
||||||
import { getMainWindow } from './window';
|
import { getMainWindow } from './window';
|
||||||
|
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
|
|
@ -15,8 +20,8 @@ let currentMetrics: {
|
||||||
gpu: GpuMetrics | null;
|
gpu: GpuMetrics | null;
|
||||||
} = {
|
} = {
|
||||||
cpu: null,
|
cpu: null,
|
||||||
memory: null,
|
|
||||||
gpu: null,
|
gpu: null,
|
||||||
|
memory: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TrayAppState {
|
interface TrayAppState {
|
||||||
|
|
@ -27,9 +32,9 @@ interface TrayAppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
const appState: TrayAppState = {
|
const appState: TrayAppState = {
|
||||||
|
currentConfig: null,
|
||||||
currentScreen: null,
|
currentScreen: null,
|
||||||
isLaunched: false,
|
isLaunched: false,
|
||||||
currentConfig: null,
|
|
||||||
monitoringEnabled: false,
|
monitoringEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,9 +63,9 @@ export function updateTrayState(state: {
|
||||||
export function updateMetrics(
|
export function updateMetrics(
|
||||||
cpu: CpuMetrics | null,
|
cpu: CpuMetrics | null,
|
||||||
memory: MemoryMetrics | null,
|
memory: MemoryMetrics | null,
|
||||||
gpu: GpuMetrics | null
|
gpu: GpuMetrics | null,
|
||||||
) {
|
) {
|
||||||
currentMetrics = { cpu, memory, gpu };
|
currentMetrics = { cpu, gpu, memory };
|
||||||
updateTrayTooltip();
|
updateTrayTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,15 +125,15 @@ function buildContextMenu() {
|
||||||
|
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
label: 'Hide Gerbil',
|
|
||||||
click: () => {
|
click: () => {
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
},
|
},
|
||||||
|
label: 'Hide Gerbil',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
label: 'Show Gerbil',
|
|
||||||
click: () => showAndFocusWindow(),
|
click: () => showAndFocusWindow(),
|
||||||
|
label: 'Show Gerbil',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,28 +142,27 @@ function buildContextMenu() {
|
||||||
if (appState.isLaunched && appState.currentScreen === 'interface') {
|
if (appState.isLaunched && appState.currentScreen === 'interface') {
|
||||||
if (appState.currentConfig) {
|
if (appState.currentConfig) {
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
label: `Running: ${stripFileExtension(appState.currentConfig || '')}`,
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
label: `Running: ${stripFileExtension(appState.currentConfig || '')}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
menuTemplate.push({
|
menuTemplate.push({
|
||||||
label: 'Eject Model',
|
|
||||||
click: () => {
|
click: () => {
|
||||||
const mainWindow = getMainWindow();
|
getMainWindow().webContents.send('tray:eject');
|
||||||
mainWindow.webContents.send('tray:eject');
|
|
||||||
},
|
},
|
||||||
|
label: 'Eject Model',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
menuTemplate.push(
|
menuTemplate.push(
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
|
||||||
click: () => {
|
click: () => {
|
||||||
app.quit();
|
app.quit();
|
||||||
},
|
},
|
||||||
}
|
label: 'Quit',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return Menu.buildFromTemplate(menuTemplate);
|
return Menu.buildFromTemplate(menuTemplate);
|
||||||
|
|
@ -179,7 +183,8 @@ export function createTray() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tray = new Tray(icon.resize({ width: 16, height: 16 }));
|
tray = new Tray(icon.resize({ height: 16, width: 16 }));
|
||||||
|
setTrayActive(true);
|
||||||
|
|
||||||
updateTrayTooltip();
|
updateTrayTooltip();
|
||||||
tray.setContextMenu(buildContextMenu());
|
tray.setContextMenu(buildContextMenu());
|
||||||
|
|
@ -218,7 +223,8 @@ export function destroyTray() {
|
||||||
if (tray && platform !== 'linux') {
|
if (tray && platform !== 'linux') {
|
||||||
tray.destroy();
|
tray.destroy();
|
||||||
tray = null;
|
tray = null;
|
||||||
|
setTrayActive(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isTrayActive = () => tray !== null;
|
export { isTrayActive } from './tray-active';
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue