mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
simplify things by switching from eslint+prettier to biome
This commit is contained in:
parent
43f6ba10a3
commit
d5b470233d
129 changed files with 1400 additions and 4925 deletions
|
|
@ -1,6 +0,0 @@
|
||||||
# Prettier-specific ignores (beyond .gitignore)
|
|
||||||
# Keep files that should be tracked by git but not formatted by prettier
|
|
||||||
|
|
||||||
.vscode/settings.json
|
|
||||||
coverage/
|
|
||||||
package-lock.json
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"endOfLine": "lf"
|
|
||||||
}
|
|
||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"recommendations": ["dbaeumer.vscode-eslint", "prettier.prettier-vscode"]
|
"recommendations": ["biomejs.biome"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
|
|
@ -27,10 +27,7 @@
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Debug Electron (Main + Renderer)",
|
"name": "Debug Electron (Main + Renderer)",
|
||||||
"configurations": [
|
"configurations": ["Debug Electron Main Process", "Debug Electron Renderer Process"]
|
||||||
"Debug Electron Main Process",
|
|
||||||
"Debug Electron Renderer Process"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
.vscode/settings.json
vendored
27
.vscode/settings.json
vendored
|
|
@ -1,20 +1,25 @@
|
||||||
{
|
{
|
||||||
"eslint.useFlatConfig": true,
|
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.biome": "explicit"
|
||||||
},
|
},
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact"
|
|
||||||
],
|
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "prettier.prettier-vscode",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "prettier.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "prettier.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
biome.json
Normal file
57
biome.json
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 100
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "warn",
|
||||||
|
"noArrayIndexKey": "off"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"noUnusedTemplateLiteral": "error",
|
||||||
|
"useConst": "error"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedVariables": "error",
|
||||||
|
"useExhaustiveDependencies": "warn"
|
||||||
|
},
|
||||||
|
"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,5 +1,5 @@
|
||||||
import { defineConfig } from 'electron-vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { defineConfig } from 'electron-vite';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
|
|
|
||||||
239
eslint.config.ts
239
eslint.config.ts
|
|
@ -1,239 +0,0 @@
|
||||||
import { defineConfig } from 'eslint/config';
|
|
||||||
import js from '@eslint/js';
|
|
||||||
import globals from 'globals';
|
|
||||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
|
||||||
import tsParser from '@typescript-eslint/parser';
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
|
||||||
import react from 'eslint-plugin-react';
|
|
||||||
import importPlugin from 'eslint-plugin-import';
|
|
||||||
import sonarjs from 'eslint-plugin-sonarjs';
|
|
||||||
// @ts-ignore - No types available
|
|
||||||
import noComments from 'eslint-plugin-no-comments';
|
|
||||||
// @ts-ignore - No types available
|
|
||||||
import promise from 'eslint-plugin-promise';
|
|
||||||
|
|
||||||
const config = defineConfig([
|
|
||||||
js.configs.recommended,
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
'dist',
|
|
||||||
'dist-electron',
|
|
||||||
'out',
|
|
||||||
'electron',
|
|
||||||
'scripts',
|
|
||||||
'release',
|
|
||||||
'build',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
|
|
||||||
ignores: [
|
|
||||||
'*.config.*',
|
|
||||||
'vite.config.*',
|
|
||||||
'eslint.config.*',
|
|
||||||
'postcss.config.*',
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
parser: tsParser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: 'module',
|
|
||||||
projectService: true,
|
|
||||||
allowDefaultProject: true,
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
'@typescript-eslint': tseslint as any,
|
|
||||||
'react-hooks': reactHooks as any,
|
|
||||||
react,
|
|
||||||
sonarjs,
|
|
||||||
'no-comments': noComments,
|
|
||||||
import: importPlugin,
|
|
||||||
promise,
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
...importPlugin.configs.recommended.rules,
|
|
||||||
...importPlugin.configs.typescript.rules,
|
|
||||||
...tseslint.configs.recommended.rules,
|
|
||||||
...tseslint.configs['recommended-type-checked'].rules,
|
|
||||||
...tseslint.configs.stylistic.rules,
|
|
||||||
|
|
||||||
'@typescript-eslint/no-unused-vars': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
argsIgnorePattern: '^_',
|
|
||||||
varsIgnorePattern: '^_',
|
|
||||||
ignoreRestSiblings: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
|
||||||
|
|
||||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-call': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-return': 'off',
|
|
||||||
|
|
||||||
'react/function-component-definition': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
namedComponents: 'arrow-function',
|
|
||||||
unnamedComponents: 'arrow-function',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'react/react-in-jsx-scope': 'off',
|
|
||||||
'react/jsx-boolean-value': ['error', 'never'],
|
|
||||||
'react/jsx-curly-brace-presence': [
|
|
||||||
'error',
|
|
||||||
{ props: 'never', children: 'never' },
|
|
||||||
],
|
|
||||||
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['react'],
|
|
||||||
importNames: ['default'],
|
|
||||||
message:
|
|
||||||
'Import specific React hooks/utilities instead of default React import. Use automatic JSX transform.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: ['fs'],
|
|
||||||
importNames: ['readFileSync', 'writeFileSync', 'existsSync'],
|
|
||||||
message:
|
|
||||||
'Use async file operations instead: readFile, writeFile, access from fs/promises',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'no-restricted-syntax': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
selector: 'CallExpression[callee.name=/.*Sync$/]',
|
|
||||||
message:
|
|
||||||
'Synchronous file operations are forbidden. Use async alternatives.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector:
|
|
||||||
'CallExpression[callee.object.object.object.name="window"][callee.object.object.property.name="electronAPI"][callee.object.property.name="config"][callee.property.name="get"] Literal[value="currentKoboldBinary"]',
|
|
||||||
message:
|
|
||||||
'Direct access to currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.getCurrentVersion() instead to get proper fallback logic.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector:
|
|
||||||
'CallExpression[callee.object.object.object.name="window"][callee.object.object.property.name="electronAPI"][callee.object.property.name="config"][callee.property.name="set"] Literal[value="currentKoboldBinary"]',
|
|
||||||
message:
|
|
||||||
'Direct setting of currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.setCurrentVersion() instead.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'import/no-default-export': 'error',
|
|
||||||
'import/prefer-default-export': 'off',
|
|
||||||
'import/no-unresolved': 'off',
|
|
||||||
'import/no-relative-parent-imports': 'error',
|
|
||||||
|
|
||||||
'@typescript-eslint/array-type': ['error', { default: 'array' }],
|
|
||||||
'@typescript-eslint/no-require-imports': 'error',
|
|
||||||
|
|
||||||
'arrow-body-style': ['error', 'as-needed'],
|
|
||||||
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
|
||||||
'object-shorthand': ['error', 'always'],
|
|
||||||
'prefer-const': 'error',
|
|
||||||
|
|
||||||
'no-console': 'error',
|
|
||||||
|
|
||||||
'no-restricted-globals': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
name: 'process',
|
|
||||||
message:
|
|
||||||
'Import specific properties from "process" instead of using the global: import { platform, env } from "process"',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'@typescript-eslint/no-inferrable-types': 'warn',
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/return-await': ['error', 'never'],
|
|
||||||
'@typescript-eslint/require-await': 'error',
|
|
||||||
'@typescript-eslint/prefer-promise-reject-errors': 'error',
|
|
||||||
|
|
||||||
'sonarjs/cognitive-complexity': ['warn', 25],
|
|
||||||
|
|
||||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
|
||||||
|
|
||||||
'promise/prefer-await-to-then': 'error',
|
|
||||||
'promise/prefer-await-to-callbacks': 'off',
|
|
||||||
'promise/no-nesting': 'error',
|
|
||||||
'promise/no-promise-in-callback': 'off',
|
|
||||||
'promise/no-callback-in-promise': 'off',
|
|
||||||
'promise/avoid-new': 'off',
|
|
||||||
'promise/no-new-statics': 'error',
|
|
||||||
'promise/no-return-wrap': 'error',
|
|
||||||
'promise/param-names': 'error',
|
|
||||||
'promise/catch-or-return': 'off',
|
|
||||||
'promise/no-native': 'off',
|
|
||||||
|
|
||||||
'no-comments/disallowComments': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.d.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'*.config.*',
|
|
||||||
'vite.config.*',
|
|
||||||
'eslint.config.*',
|
|
||||||
'postcss.config.*',
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parser: tsParser,
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'import/no-default-export': 'off',
|
|
||||||
'@typescript-eslint/return-await': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-require-imports': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.d.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-comments/disallowComments': 'off',
|
|
||||||
'import/no-default-export': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
24
package.json
24
package.json
|
|
@ -6,8 +6,7 @@
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0",
|
"node": ">=22.0.0"
|
||||||
"yarn": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.12.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -15,12 +14,8 @@
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"package": "electron-vite build && electron-builder --publish=never",
|
"package": "electron-vite build && electron-builder --publish=never",
|
||||||
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
|
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
|
||||||
"format": "prettier --write . --ignore-path .gitignore | grep -v '(unchanged)' || true",
|
"check": "biome check . && tsc --noEmit",
|
||||||
"format:check": "prettier --check . --ignore-path .gitignore",
|
"fix": "biome check --write .",
|
||||||
"lint": "eslint .",
|
|
||||||
"lint:fix": "eslint . --fix",
|
|
||||||
"compile": "tsc --noEmit",
|
|
||||||
"check": "yarn format:check && yarn lint && yarn compile",
|
|
||||||
"release": "yarn dlx tsx scripts/release.ts"
|
"release": "yarn dlx tsx scripts/release.ts"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
@ -65,29 +60,18 @@
|
||||||
"zustand": "^5.0.10"
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@biomejs/biome": "^2.3.11",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/node": "^25.0.6",
|
"@types/node": "^25.0.6",
|
||||||
"@types/react": "^19.2.8",
|
"@types/react": "^19.2.8",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
||||||
"@typescript-eslint/parser": "^8.53.0",
|
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"electron": "^38.7.2",
|
"electron": "^38.7.2",
|
||||||
"electron-builder": "^26.4.0",
|
"electron-builder": "^26.4.0",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"eslint-plugin-import": "^2.32.0",
|
|
||||||
"eslint-plugin-no-comments": "^1.1.10",
|
|
||||||
"eslint-plugin-promise": "^7.2.1",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-sonarjs": "^3.0.5",
|
|
||||||
"globals": "^17.0.0",
|
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"prettier": "^3.7.4",
|
|
||||||
"rollup-plugin-visualizer": "^6.0.5",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ try {
|
||||||
|
|
||||||
console.log(`✅ Release ${tagName} created successfully!`);
|
console.log(`✅ Release ${tagName} created successfully!`);
|
||||||
console.log(`📦 GitHub Actions will now build and publish the release.`);
|
console.log(`📦 GitHub Actions will now build and publish the release.`);
|
||||||
console.log(
|
console.log(`🔗 Check the progress at: https://github.com/lone-cloud/gerbil/actions`);
|
||||||
`🔗 Check the progress at: https://github.com/lone-cloud/gerbil/actions`
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
console.error('❌ Error creating release:', errorMessage);
|
console.error('❌ Error creating release:', errorMessage);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Text, Group, Button, Stack } 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';
|
||||||
|
|
||||||
|
|
@ -26,10 +26,7 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
|
||||||
SIGHUP: 'The process lost its controlling terminal',
|
SIGHUP: 'The process lost its controlling terminal',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return signalDescriptions[crashInfo.signal] || `Terminated by signal ${crashInfo.signal}`;
|
||||||
signalDescriptions[crashInfo.signal] ||
|
|
||||||
`Terminated by signal ${crashInfo.signal}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (crashInfo.exitCode !== null) {
|
if (crashInfo.exitCode !== null) {
|
||||||
|
|
@ -39,11 +36,7 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
|
||||||
return 'The process terminated unexpectedly';
|
return 'The process terminated unexpectedly';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BackendCrashModal = ({
|
export const BackendCrashModal = ({ opened, onClose, crashInfo }: BackendCrashModalProps) => {
|
||||||
opened,
|
|
||||||
onClose,
|
|
||||||
crashInfo,
|
|
||||||
}: BackendCrashModalProps) => {
|
|
||||||
if (!crashInfo) return null;
|
if (!crashInfo) return null;
|
||||||
|
|
||||||
const description = getCrashDescription(crashInfo);
|
const description = getCrashDescription(crashInfo);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { Button, Checkbox, Group, Stack, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Text, Group, Button, Checkbox, Stack } from '@mantine/core';
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface EjectConfirmModalProps {
|
interface EjectConfirmModalProps {
|
||||||
|
|
@ -8,11 +8,7 @@ interface EjectConfirmModalProps {
|
||||||
onConfirm: (skipConfirmation: boolean) => void;
|
onConfirm: (skipConfirmation: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EjectConfirmModal = ({
|
export const EjectConfirmModal = ({ opened, onClose, onConfirm }: EjectConfirmModalProps) => {
|
||||||
opened,
|
|
||||||
onClose,
|
|
||||||
onConfirm,
|
|
||||||
}: EjectConfirmModalProps) => {
|
|
||||||
const [skipConfirmation, setSkipConfirmation] = useState(false);
|
const [skipConfirmation, setSkipConfirmation] = useState(false);
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
|
|
@ -26,15 +22,10 @@ export const EjectConfirmModal = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={opened} onClose={handleClose} title="Are you sure you want to eject?">
|
||||||
opened={opened}
|
|
||||||
onClose={handleClose}
|
|
||||||
title="Are you sure you want to eject?"
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
This will terminate the running process and return to the launch
|
This will terminate the running process and return to the launch screen.
|
||||||
screen.
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ReactNode } from 'react';
|
import { Alert, Button, Center, rem, Stack, Text } from '@mantine/core';
|
||||||
import { Center, Stack, Text, Button, Alert, rem } from '@mantine/core';
|
|
||||||
import { AlertTriangle, FolderOpen } from 'lucide-react';
|
import { AlertTriangle, FolderOpen } from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
|
|
@ -24,8 +24,8 @@ const ErrorFallback = ({
|
||||||
|
|
||||||
<Alert color="red" title="Something went wrong" w="100%">
|
<Alert color="red" title="Something went wrong" w="100%">
|
||||||
<Text size="sm" mb="md">
|
<Text size="sm" mb="md">
|
||||||
The application encountered an unexpected error and crashed. You can
|
The application encountered an unexpected error and crashed. You can view the error
|
||||||
view the error details in the logs folder.
|
details in the logs folder.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="xs" c="dimmed" style={{ fontFamily: 'monospace' }} mb="sm">
|
<Text size="xs" c="dimmed" style={{ fontFamily: 'monospace' }} mb="sm">
|
||||||
|
|
@ -35,9 +35,7 @@ const ErrorFallback = ({
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={
|
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
||||||
>
|
>
|
||||||
Show Logs Folder
|
Show Logs Folder
|
||||||
|
|
@ -68,10 +66,7 @@ export const ErrorBoundary = ({ children }: ErrorBoundaryProps) => (
|
||||||
<ReactErrorBoundary
|
<ReactErrorBoundary
|
||||||
FallbackComponent={ErrorFallback}
|
FallbackComponent={ErrorFallback}
|
||||||
onError={(error) => {
|
onError={(error) => {
|
||||||
window.electronAPI?.logs?.logError(
|
window.electronAPI?.logs?.logError('App crashed with unhandled error:', error);
|
||||||
'App crashed with unhandled error:',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Tooltip, ActionIcon } from '@mantine/core';
|
import { ActionIcon, Button, Tooltip } from '@mantine/core';
|
||||||
import { Activity } from 'lucide-react';
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
interface PerformanceBadgeProps {
|
interface PerformanceBadgeProps {
|
||||||
|
|
@ -18,20 +18,14 @@ export const PerformanceBadge = ({
|
||||||
const result = await window.electronAPI.app.openPerformanceManager();
|
const result = await window.electronAPI.app.openPerformanceManager();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
window.electronAPI.logs.logError(
|
window.electronAPI.logs.logError(`Failed to open performance manager: ${result.error}`);
|
||||||
`Failed to open performance manager: ${result.error}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (iconOnly) {
|
if (iconOnly) {
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltipLabel} position="top">
|
<Tooltip label={tooltipLabel} position="top">
|
||||||
<ActionIcon
|
<ActionIcon size="sm" variant="subtle" onClick={() => void handlePerformanceClick()}>
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => void handlePerformanceClick()}
|
|
||||||
>
|
|
||||||
<Activity size="1.125rem" />
|
<Activity size="1.125rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ScreenTransition } from '@/components/App/ScreenTransition';
|
import { ScreenTransition } from '@/components/App/ScreenTransition';
|
||||||
import { DownloadScreen } from '@/components/screens/Download';
|
import { DownloadScreen } from '@/components/screens/Download';
|
||||||
import { LaunchScreen } from '@/components/screens/Launch';
|
|
||||||
import { InterfaceScreen } from '@/components/screens/Interface';
|
import { InterfaceScreen } from '@/components/screens/Interface';
|
||||||
|
import { LaunchScreen } from '@/components/screens/Launch';
|
||||||
import { WelcomeScreen } from '@/components/screens/Welcome';
|
import { WelcomeScreen } from '@/components/screens/Welcome';
|
||||||
import type { InterfaceTab, Screen } from '@/types';
|
import type { InterfaceTab, Screen } from '@/types';
|
||||||
|
|
||||||
|
|
@ -28,35 +28,20 @@ export const AppRouter = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScreenTransition
|
<ScreenTransition isActive={currentScreen === 'welcome'} shouldAnimate={hasInitialized}>
|
||||||
isActive={currentScreen === 'welcome'}
|
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<WelcomeScreen onGetStarted={onWelcomeComplete} />
|
<WelcomeScreen onGetStarted={onWelcomeComplete} />
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
|
|
||||||
<ScreenTransition
|
<ScreenTransition isActive={currentScreen === 'download'} shouldAnimate={hasInitialized}>
|
||||||
isActive={currentScreen === 'download'}
|
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<DownloadScreen onDownloadComplete={onDownloadComplete} />
|
<DownloadScreen onDownloadComplete={onDownloadComplete} />
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
|
|
||||||
<ScreenTransition
|
<ScreenTransition isActive={currentScreen === 'launch'} shouldAnimate={hasInitialized}>
|
||||||
isActive={currentScreen === 'launch'}
|
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<LaunchScreen onLaunch={onLaunch} />
|
<LaunchScreen onLaunch={onLaunch} />
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
|
|
||||||
<ScreenTransition
|
<ScreenTransition isActive={isInterfaceScreen} shouldAnimate={hasInitialized}>
|
||||||
isActive={isInterfaceScreen}
|
<InterfaceScreen activeTab={activeInterfaceTab} isServerReady={isServerReady} />
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<InterfaceScreen
|
|
||||||
activeTab={activeInterfaceTab}
|
|
||||||
isServerReady={isServerReady}
|
|
||||||
/>
|
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Transition } from '@mantine/core';
|
import { Transition } from '@mantine/core';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface ScreenTransitionProps {
|
interface ScreenTransitionProps {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
@ -7,11 +7,7 @@ interface ScreenTransitionProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScreenTransition = ({
|
export const ScreenTransition = ({ isActive, shouldAnimate, children }: ScreenTransitionProps) => (
|
||||||
isActive,
|
|
||||||
shouldAnimate,
|
|
||||||
children,
|
|
||||||
}: ScreenTransitionProps) => (
|
|
||||||
<Transition
|
<Transition
|
||||||
mounted={isActive}
|
mounted={isActive}
|
||||||
transition="fade"
|
transition="fade"
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,16 @@
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { ActionIcon, AppShell, CopyButton, Group, Tooltip } from '@mantine/core';
|
||||||
import {
|
import { Check, Globe, NotepadText } from 'lucide-react';
|
||||||
Group,
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
AppShell,
|
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
|
||||||
ActionIcon,
|
|
||||||
Tooltip,
|
|
||||||
CopyButton,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { NotepadText, Globe, Check } from 'lucide-react';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { getTunnelInterfaceUrl } from '@/utils/interface';
|
import { getTunnelInterfaceUrl } from '@/utils/interface';
|
||||||
import type {
|
|
||||||
CpuMetrics,
|
|
||||||
MemoryMetrics,
|
|
||||||
GpuMetrics,
|
|
||||||
} from '@/main/modules/monitoring';
|
|
||||||
import { PerformanceBadge } from './PerformanceBadge';
|
import { PerformanceBadge } from './PerformanceBadge';
|
||||||
|
|
||||||
interface StatusBarProps {
|
export const StatusBar = () => {
|
||||||
maxDataPoints?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
|
||||||
const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null);
|
const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null);
|
||||||
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(
|
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
|
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
|
||||||
const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
|
const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
|
||||||
const {
|
const {
|
||||||
|
|
@ -40,10 +24,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
|
|
||||||
const tunnelUrl = useMemo(() => {
|
const tunnelUrl = useMemo(() => {
|
||||||
if (!tunnelBaseUrl) return null;
|
if (!tunnelBaseUrl) return null;
|
||||||
if (
|
if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') {
|
||||||
frontendPreference === 'sillytavern' ||
|
|
||||||
frontendPreference === 'openwebui'
|
|
||||||
) {
|
|
||||||
return tunnelBaseUrl;
|
return tunnelBaseUrl;
|
||||||
}
|
}
|
||||||
return getTunnelInterfaceUrl(tunnelBaseUrl, {
|
return getTunnelInterfaceUrl(tunnelBaseUrl, {
|
||||||
|
|
@ -51,12 +32,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
imageGenerationFrontendPreference,
|
imageGenerationFrontendPreference,
|
||||||
isImageGenerationMode,
|
isImageGenerationMode,
|
||||||
});
|
});
|
||||||
}, [
|
}, [tunnelBaseUrl, frontendPreference, imageGenerationFrontendPreference, isImageGenerationMode]);
|
||||||
tunnelBaseUrl,
|
|
||||||
frontendPreference,
|
|
||||||
imageGenerationFrontendPreference,
|
|
||||||
isImageGenerationMode,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!systemMonitoringEnabled) {
|
if (!systemMonitoringEnabled) {
|
||||||
|
|
@ -80,12 +56,9 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
setGpuMetrics(metrics);
|
setGpuMetrics(metrics);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanupCpu =
|
const cleanupCpu = window.electronAPI.monitoring.onCpuMetrics(handleCpuMetrics);
|
||||||
window.electronAPI.monitoring.onCpuMetrics(handleCpuMetrics);
|
const cleanupMemory = window.electronAPI.monitoring.onMemoryMetrics(handleMemoryMetrics);
|
||||||
const cleanupMemory =
|
const cleanupGpu = window.electronAPI.monitoring.onGpuMetrics(handleGpuMetrics);
|
||||||
window.electronAPI.monitoring.onMemoryMetrics(handleMemoryMetrics);
|
|
||||||
const cleanupGpu =
|
|
||||||
window.electronAPI.monitoring.onGpuMetrics(handleGpuMetrics);
|
|
||||||
const stopMonitoring = window.electronAPI.monitoring.start();
|
const stopMonitoring = window.electronAPI.monitoring.start();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -95,11 +68,10 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
cleanupGpu?.();
|
cleanupGpu?.();
|
||||||
stopMonitoring?.();
|
stopMonitoring?.();
|
||||||
};
|
};
|
||||||
}, [maxDataPoints, systemMonitoringEnabled]);
|
}, [systemMonitoringEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup =
|
const cleanup = window.electronAPI.kobold.onTunnelUrlChanged(setTunnelBaseUrl);
|
||||||
window.electronAPI.kobold.onTunnelUrlChanged(setTunnelBaseUrl);
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -133,21 +105,14 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
{tunnelUrl && (
|
{tunnelUrl && (
|
||||||
<CopyButton value={tunnelUrl}>
|
<CopyButton value={tunnelUrl}>
|
||||||
{({ copied, copy }) => (
|
{({ copied, copy }) => (
|
||||||
<Tooltip
|
<Tooltip label={copied ? 'Copied!' : 'Copy Tunnel URL'} position="top">
|
||||||
label={copied ? 'Copied!' : 'Copy Tunnel URL'}
|
|
||||||
position="top"
|
|
||||||
>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
color={copied ? 'teal' : undefined}
|
color={copied ? 'teal' : undefined}
|
||||||
onClick={copy}
|
onClick={copy}
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? <Check size="1.25rem" /> : <Globe size="1.25rem" />}
|
||||||
<Check size="1.25rem" />
|
|
||||||
) : (
|
|
||||||
<Globe size="1.25rem" />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
import {
|
import { ActionIcon, AppShell, Box, Group, Image, Tooltip } from '@mantine/core';
|
||||||
Group,
|
import { Copy, Minus, Settings, Square, X } from 'lucide-react';
|
||||||
ActionIcon,
|
import { useEffect, useState } from 'react';
|
||||||
Box,
|
import { UpdateButton } from '@/components/App/UpdateButton';
|
||||||
Image,
|
import { Select } from '@/components/Select';
|
||||||
AppShell,
|
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||||
Tooltip,
|
import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
} from '@mantine/core';
|
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
||||||
import { Minus, Square, X, Copy, Settings } from 'lucide-react';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { getAvailableInterfaceOptions } from '@/utils/interface';
|
|
||||||
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
|
||||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
|
||||||
import { UpdateButton } from '@/components/App/UpdateButton';
|
|
||||||
import icon from '/icon.png';
|
|
||||||
import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
|
|
||||||
import type { InterfaceTab, Screen, SelectOption } from '@/types';
|
import type { InterfaceTab, Screen, SelectOption } from '@/types';
|
||||||
import { Select } from '@/components/Select';
|
import { getAvailableInterfaceOptions } from '@/utils/interface';
|
||||||
|
import icon from '/icon.png';
|
||||||
|
|
||||||
interface TitleBarProps {
|
interface TitleBarProps {
|
||||||
currentScreen: Screen;
|
currentScreen: Screen;
|
||||||
|
|
@ -26,12 +19,7 @@ interface TitleBarProps {
|
||||||
onTabChange: (tab: InterfaceTab) => void;
|
onTabChange: (tab: InterfaceTab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TitleBar = ({
|
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
|
||||||
currentScreen,
|
|
||||||
currentTab,
|
|
||||||
onEject,
|
|
||||||
onTabChange,
|
|
||||||
}: TitleBarProps) => {
|
|
||||||
const {
|
const {
|
||||||
resolvedColorScheme: colorScheme,
|
resolvedColorScheme: colorScheme,
|
||||||
frontendPreference,
|
frontendPreference,
|
||||||
|
|
@ -56,8 +44,7 @@ export const TitleBar = ({
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
color:
|
color: option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
|
||||||
option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
|
|
||||||
fontWeight: option.value === 'eject' ? 600 : undefined,
|
fontWeight: option.value === 'eject' ? 600 : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -81,9 +68,7 @@ export const TitleBar = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell.Header
|
<AppShell.Header style={{ display: 'flex', flexDirection: 'column', border: 'none' }}>
|
||||||
style={{ display: 'flex', flexDirection: 'column', border: 'none' }}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
height: TITLEBAR_HEIGHT,
|
height: TITLEBAR_HEIGHT,
|
||||||
|
|
@ -92,20 +77,14 @@ export const TitleBar = ({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
colorScheme === 'dark'
|
colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
|
||||||
? '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',
|
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group
|
<Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}>
|
||||||
gap="0.5rem"
|
|
||||||
align="center"
|
|
||||||
style={{ WebkitAppRegion: 'no-drag' }}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
src={icon}
|
src={icon}
|
||||||
alt={PRODUCT_NAME}
|
alt={PRODUCT_NAME}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,14 @@
|
||||||
import {
|
import { Anchor, Button, Card, Group, Loader, Progress, Stack, Text } from '@mantine/core';
|
||||||
Stack,
|
import { Download, ExternalLink, X } from 'lucide-react';
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Loader,
|
|
||||||
Anchor,
|
|
||||||
Progress,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Download, X, ExternalLink } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { DownloadItem } from '@/types/electron';
|
import { Modal } from '@/components/Modal';
|
||||||
|
import { GITHUB_API } from '@/constants';
|
||||||
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
|
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
|
import type { DownloadItem } from '@/types/electron';
|
||||||
import { pretifyBinName } from '@/utils/assets';
|
import { pretifyBinName } from '@/utils/assets';
|
||||||
import { formatDownloadSize } from '@/utils/format';
|
import { formatDownloadSize } from '@/utils/format';
|
||||||
import { GITHUB_API } from '@/constants';
|
|
||||||
import { safeExecute } from '@/utils/logger';
|
import { safeExecute } from '@/utils/logger';
|
||||||
import { Modal } from '@/components/Modal';
|
|
||||||
|
|
||||||
interface UpdateAvailableModalProps {
|
interface UpdateAvailableModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -39,11 +30,8 @@ export const UpdateAvailableModal = ({
|
||||||
const availableUpdate = updateInfo?.availableUpdate;
|
const availableUpdate = updateInfo?.availableUpdate;
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
const isDownloading =
|
const isDownloading = !!availableUpdate && downloading === availableUpdate.name;
|
||||||
!!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 () => {
|
||||||
if (availableUpdate) {
|
if (availableUpdate) {
|
||||||
|
|
@ -99,8 +87,7 @@ export const UpdateAvailableModal = ({
|
||||||
|
|
||||||
{availableUpdate?.size && (
|
{availableUpdate?.size && (
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
Update Size:{' '}
|
Update Size: {formatDownloadSize(availableUpdate.size, availableUpdate.url)}
|
||||||
{formatDownloadSize(availableUpdate.size, availableUpdate.url)}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -158,11 +145,7 @@ export const UpdateAvailableModal = ({
|
||||||
loading={isDownloading || isUpdating}
|
loading={isDownloading || isUpdating}
|
||||||
disabled={isDownloading || isUpdating}
|
disabled={isDownloading || isUpdating}
|
||||||
leftSection={
|
leftSection={
|
||||||
isDownloading || isUpdating ? (
|
isDownloading || isUpdating ? <Loader size="1rem" /> : <Download size={16} />
|
||||||
<Loader size="1rem" />
|
|
||||||
) : (
|
|
||||||
<Download size={16} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
color="orange"
|
color="orange"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +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 { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
import { type MouseEvent, useState } from 'react';
|
||||||
import { TITLEBAR_HEIGHT } from '@/constants';
|
import { TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { useState, type MouseEvent } from 'react';
|
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
||||||
|
|
||||||
export const UpdateButton = () => {
|
export const UpdateButton = () => {
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { AppShell, Center, Loader, Stack, Text, useMantineColorScheme } from '@mantine/core';
|
||||||
import {
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
AppShell,
|
|
||||||
Loader,
|
|
||||||
Center,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
useMantineColorScheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal';
|
|
||||||
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
|
|
||||||
import { BackendCrashModal } from '@/components/App/BackendCrashModal';
|
import { BackendCrashModal } from '@/components/App/BackendCrashModal';
|
||||||
import { TitleBar } from '@/components/App/TitleBar';
|
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
|
||||||
import { StatusBar } from '@/components/App/StatusBar';
|
|
||||||
import { ErrorBoundary } from '@/components/App/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/App/ErrorBoundary';
|
||||||
import { AppRouter } from '@/components/App/Router';
|
import { AppRouter } from '@/components/App/Router';
|
||||||
|
import { StatusBar } from '@/components/App/StatusBar';
|
||||||
|
import { TitleBar } from '@/components/App/TitleBar';
|
||||||
|
import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal';
|
||||||
import { NotepadContainer } from '@/components/Notepad/Container';
|
import { NotepadContainer } from '@/components/Notepad/Container';
|
||||||
|
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { getDefaultInterfaceTab } from '@/utils/interface';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
|
||||||
import type { DownloadItem } from '@/types/electron';
|
|
||||||
import type { InterfaceTab, Screen } from '@/types';
|
import type { InterfaceTab, Screen } from '@/types';
|
||||||
|
import type { DownloadItem } from '@/types/electron';
|
||||||
import type { KoboldCrashInfo } from '@/types/ipc';
|
import type { KoboldCrashInfo } from '@/types/ipc';
|
||||||
|
import { getDefaultInterfaceTab } from '@/utils/interface';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
||||||
const [hasInitialized, setHasInitialized] = useState(false);
|
const [hasInitialized, setHasInitialized] = useState(false);
|
||||||
const [activeInterfaceTab, setActiveInterfaceTab] =
|
const [activeInterfaceTab, setActiveInterfaceTab] = useState<InterfaceTab>('terminal');
|
||||||
useState<InterfaceTab>('terminal');
|
|
||||||
const [isServerReady, setIsServerReady] = useState(false);
|
const [isServerReady, setIsServerReady] = useState(false);
|
||||||
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
|
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
|
||||||
const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null);
|
const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null);
|
||||||
|
|
@ -42,8 +34,7 @@ export const App = () => {
|
||||||
imageGenerationFrontendPreference,
|
imageGenerationFrontendPreference,
|
||||||
} = usePreferencesStore();
|
} = usePreferencesStore();
|
||||||
const { setColorScheme } = useMantineColorScheme();
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
const { model, sdmodel, isTextMode, isImageGenerationMode } =
|
const { model, sdmodel, isTextMode, isImageGenerationMode } = useLaunchConfigStore();
|
||||||
useLaunchConfigStore();
|
|
||||||
|
|
||||||
const defaultInterfaceTab = useMemo(
|
const defaultInterfaceTab = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -53,12 +44,7 @@ export const App = () => {
|
||||||
isTextMode,
|
isTextMode,
|
||||||
isImageGenerationMode,
|
isImageGenerationMode,
|
||||||
}),
|
}),
|
||||||
[
|
[frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode]
|
||||||
frontendPreference,
|
|
||||||
imageGenerationFrontendPreference,
|
|
||||||
isTextMode,
|
|
||||||
isImageGenerationMode,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -80,9 +66,7 @@ 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
|
const modelName = displayModel ? displayModel.split('/').pop() || displayModel : null;
|
||||||
? displayModel.split('/').pop() || displayModel
|
|
||||||
: null;
|
|
||||||
|
|
||||||
void window.electronAPI.app.updateTrayState({
|
void window.electronAPI.app.updateTrayState({
|
||||||
screen: currentScreen,
|
screen: currentScreen,
|
||||||
|
|
@ -95,12 +79,12 @@ export const App = () => {
|
||||||
void updateTray();
|
void updateTray();
|
||||||
}, [currentScreen, model, sdmodel, systemMonitoringEnabled]);
|
}, [currentScreen, model, sdmodel, systemMonitoringEnabled]);
|
||||||
|
|
||||||
const performEject = () => {
|
const performEject = useCallback(() => {
|
||||||
window.electronAPI.kobold.stopKoboldCpp();
|
window.electronAPI.kobold.stopKoboldCpp();
|
||||||
setIsServerReady(false);
|
setIsServerReady(false);
|
||||||
setActiveInterfaceTab('terminal');
|
setActiveInterfaceTab('terminal');
|
||||||
setCurrentScreen('launch');
|
setCurrentScreen('launch');
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ejectCleanup = window.electronAPI.app.onTrayEject(() => {
|
const ejectCleanup = window.electronAPI.app.onTrayEject(() => {
|
||||||
|
|
@ -110,14 +94,12 @@ export const App = () => {
|
||||||
return () => {
|
return () => {
|
||||||
ejectCleanup();
|
ejectCleanup();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [performEject]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const crashCleanup = window.electronAPI.kobold.onKoboldCrashed(
|
const crashCleanup = window.electronAPI.kobold.onKoboldCrashed((crashData) => {
|
||||||
(crashData) => {
|
|
||||||
setCrashInfo(crashData);
|
setCrashInfo(crashData);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
crashCleanup();
|
crashCleanup();
|
||||||
|
|
@ -134,10 +116,7 @@ export const App = () => {
|
||||||
|
|
||||||
const { handleDownload, loadingRemote } = useKoboldBackendsStore();
|
const { handleDownload, loadingRemote } = useKoboldBackendsStore();
|
||||||
|
|
||||||
const determineScreen = (
|
const determineScreen = useCallback((currentVersion: unknown, hasSeenWelcome: boolean) => {
|
||||||
currentVersion: unknown,
|
|
||||||
hasSeenWelcome: boolean
|
|
||||||
) => {
|
|
||||||
if (!hasSeenWelcome) {
|
if (!hasSeenWelcome) {
|
||||||
setCurrentScreen('welcome');
|
setCurrentScreen('welcome');
|
||||||
} else if (currentVersion) {
|
} else if (currentVersion) {
|
||||||
|
|
@ -145,7 +124,7 @@ export const App = () => {
|
||||||
} else {
|
} else {
|
||||||
setCurrentScreen('download');
|
setCurrentScreen('download');
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkInstallation = async () => {
|
const checkInstallation = async () => {
|
||||||
|
|
@ -159,14 +138,13 @@ export const App = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
void checkInstallation();
|
void checkInstallation();
|
||||||
}, []);
|
}, [determineScreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingRemote || !hasInitialized) return;
|
if (loadingRemote || !hasInitialized) return;
|
||||||
|
|
||||||
const runUpdateCheck = async () => {
|
const runUpdateCheck = async () => {
|
||||||
const currentBackend =
|
const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
|
||||||
await window.electronAPI.kobold.getCurrentBackend();
|
|
||||||
if (!currentBackend) return;
|
if (!currentBackend) return;
|
||||||
|
|
||||||
void checkForUpdates();
|
void checkForUpdates();
|
||||||
|
|
@ -203,9 +181,7 @@ export const App = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEject = async () => {
|
const handleEject = async () => {
|
||||||
const skipEjectConfirmation = await window.electronAPI.config.get(
|
const skipEjectConfirmation = await window.electronAPI.config.get('skipEjectConfirmation');
|
||||||
'skipEjectConfirmation'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (skipEjectConfirmation) {
|
if (skipEjectConfirmation) {
|
||||||
performEject();
|
performEject();
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,10 @@
|
||||||
import {
|
import { Badge, Button, Card, Group, Loader, Progress, rem, Stack, Text } from '@mantine/core';
|
||||||
Card,
|
|
||||||
Stack,
|
|
||||||
Group,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Loader,
|
|
||||||
Progress,
|
|
||||||
rem,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Download, Trash2 } from 'lucide-react';
|
import { Download, Trash2 } from 'lucide-react';
|
||||||
import { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import type { BackendInfo } from '@/types';
|
import type { BackendInfo } from '@/types';
|
||||||
|
import { isWindowsROCmBuild, pretifyBinName } from '@/utils/assets';
|
||||||
|
|
||||||
interface DownloadCardProps {
|
interface DownloadCardProps {
|
||||||
backend: BackendInfo;
|
backend: BackendInfo;
|
||||||
|
|
@ -43,16 +33,11 @@ export const DownloadCard = ({
|
||||||
const { downloading, downloadProgress } = useKoboldBackendsStore();
|
const { downloading, downloadProgress } = useKoboldBackendsStore();
|
||||||
|
|
||||||
const isLoading = downloading === backend.name;
|
const isLoading = downloading === backend.name;
|
||||||
const currentProgress = isLoading
|
const currentProgress = isLoading ? Math.min(downloadProgress[backend.name], 100) || 0 : 0;
|
||||||
? Math.min(downloadProgress[backend.name], 100) || 0
|
|
||||||
: 0;
|
|
||||||
const hasVersionMismatch = Boolean(
|
const hasVersionMismatch = Boolean(
|
||||||
backend.version &&
|
backend.version && backend.actualVersion && backend.version !== backend.actualVersion
|
||||||
backend.actualVersion &&
|
|
||||||
backend.version !== backend.actualVersion
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const renderActionButtons = () => {
|
const renderActionButtons = () => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { Anchor, Box, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Text, Anchor, Box } from '@mantine/core';
|
|
||||||
|
|
||||||
interface ImportBackendLinkProps {
|
interface ImportBackendLinkProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
import {
|
import { ActionIcon, Card, Group, rem, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
rem,
|
|
||||||
ActionIcon,
|
|
||||||
Tooltip,
|
|
||||||
Card,
|
|
||||||
Stack,
|
|
||||||
Group,
|
|
||||||
Text,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Copy } from 'lucide-react';
|
import { Copy } from 'lucide-react';
|
||||||
import { safeExecute } from '@/utils/logger';
|
import { safeExecute } from '@/utils/logger';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,7 @@ interface InfoTooltipProps {
|
||||||
width?: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfoTooltip = ({
|
export const InfoTooltip = ({ label, multiline = true, width = 300 }: InfoTooltipProps) => (
|
||||||
label,
|
|
||||||
multiline = true,
|
|
||||||
width = 300,
|
|
||||||
}: InfoTooltipProps) => (
|
|
||||||
<Tooltip label={label} multiline={multiline} w={width}>
|
<Tooltip label={label} multiline={multiline} w={width}>
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
<Info size={14} />
|
<Info size={14} />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Modal as MantineModal, Button, Box } from '@mantine/core';
|
import { Box, Button, Modal as MantineModal } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
const TITLEBAR_HEIGHT = '2.5rem';
|
const TITLEBAR_HEIGHT = '2.5rem';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Text, Button, Group } from '@mantine/core';
|
import { Button, Group, Text } from '@mantine/core';
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface CloseConfirmModalProps {
|
interface CloseConfirmModalProps {
|
||||||
|
|
@ -16,8 +16,8 @@ export const CloseConfirmModal = ({
|
||||||
}: CloseConfirmModalProps) => (
|
}: CloseConfirmModalProps) => (
|
||||||
<Modal opened={isOpen} onClose={onCancel} title="Confirm Close Tab" size="sm">
|
<Modal opened={isOpen} onClose={onCancel} title="Confirm Close Tab" size="sm">
|
||||||
<Text size="sm" mb="md">
|
<Text size="sm" mb="md">
|
||||||
The tab “{tabTitle}” contains content. Closing it will
|
The tab “{tabTitle}” contains content. Closing it will permanently delete this
|
||||||
permanently delete this data. Are you sure you want to close it?
|
data. Are you sure you want to close it?
|
||||||
</Text>
|
</Text>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="subtle" onClick={onCancel}>
|
<Button variant="subtle" onClick={onCancel}>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useEffect, useRef, useState, type MouseEvent } from 'react';
|
import { ActionIcon, Box, Paper } from '@mantine/core';
|
||||||
import { Box, Paper, ActionIcon } from '@mantine/core';
|
|
||||||
import { Minus } from 'lucide-react';
|
import { Minus } from 'lucide-react';
|
||||||
|
import { type MouseEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
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 { NOTEPAD_MIN_WIDTH, NOTEPAD_MIN_HEIGHT } from '@/constants/notepad';
|
|
||||||
import { NotepadTabs } from './Tabs.tsx';
|
|
||||||
import { NotepadEditor } from './Editor.tsx';
|
|
||||||
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
||||||
|
import { NotepadEditor } from './Editor.tsx';
|
||||||
|
import { NotepadTabs } from './Tabs.tsx';
|
||||||
|
|
||||||
export const NotepadContainer = () => {
|
export const NotepadContainer = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -62,8 +62,7 @@ export const NotepadContainer = () => {
|
||||||
setConfirmCloseModal({ isOpen: false, title: '' });
|
setConfirmCloseModal({ isOpen: false, title: '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResizeStart =
|
const handleResizeStart = (direction: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||||
(direction: string) => (e: MouseEvent<HTMLDivElement>) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setResizeDirection(direction);
|
setResizeDirection(direction);
|
||||||
};
|
};
|
||||||
|
|
@ -81,10 +80,7 @@ export const NotepadContainer = () => {
|
||||||
newWidth = Math.max(NOTEPAD_MIN_WIDTH, e.clientX - rect.left);
|
newWidth = Math.max(NOTEPAD_MIN_WIDTH, e.clientX - rect.left);
|
||||||
}
|
}
|
||||||
if (resizeDirection.includes('top')) {
|
if (resizeDirection.includes('top')) {
|
||||||
newHeight = Math.max(
|
newHeight = Math.max(NOTEPAD_MIN_HEIGHT, window.innerHeight - e.clientY - 5);
|
||||||
NOTEPAD_MIN_HEIGHT,
|
|
||||||
window.innerHeight - e.clientY - 5
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPosition({
|
setPosition({
|
||||||
|
|
@ -179,8 +175,7 @@ export const NotepadContainer = () => {
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
cursor:
|
cursor:
|
||||||
resizeDirection.includes('right') &&
|
resizeDirection.includes('right') && resizeDirection.includes('top')
|
||||||
resizeDirection.includes('top')
|
|
||||||
? 'ne-resize'
|
? 'ne-resize'
|
||||||
: resizeDirection.includes('right')
|
: resizeDirection.includes('right')
|
||||||
? 'ew-resize'
|
? 'ew-resize'
|
||||||
|
|
@ -203,10 +198,7 @@ export const NotepadContainer = () => {
|
||||||
minHeight: 28,
|
minHeight: 28,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NotepadTabs
|
<NotepadTabs onCreateNewTab={handleCreateNewTab} onCloseTab={handleTabCloseRequest} />
|
||||||
onCreateNewTab={handleCreateNewTab}
|
|
||||||
onCloseTab={handleTabCloseRequest}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -217,11 +209,7 @@ export const NotepadContainer = () => {
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>
|
||||||
variant="subtle"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => setVisible(false)}
|
|
||||||
>
|
|
||||||
<Minus size="1rem" />
|
<Minus size="1rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,10 @@
|
||||||
import {
|
import { history, historyKeymap } from '@codemirror/commands';
|
||||||
useCallback,
|
import { search } from '@codemirror/search';
|
||||||
useEffect,
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
useState,
|
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
|
||||||
useRef,
|
|
||||||
type MouseEvent,
|
|
||||||
} from 'react';
|
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { search } from '@codemirror/search';
|
import { type MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
|
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
|
||||||
import { history, historyKeymap } from '@codemirror/commands';
|
|
||||||
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';
|
||||||
|
|
@ -20,13 +14,10 @@ interface NotepadEditorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
const { saveTabContent, showLineNumbers, setShowLineNumbers } =
|
const { saveTabContent, showLineNumbers, setShowLineNumbers } = useNotepadStore();
|
||||||
useNotepadStore();
|
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
const { resolvedColorScheme } = usePreferencesStore();
|
||||||
const [content, setContent] = useState(() => tab.content);
|
const [content, setContent] = useState(() => tab.content);
|
||||||
const [saveTimeout, setSaveTimeout] = useState<ReturnType<
|
const [saveTimeout, setSaveTimeout] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||||
typeof setTimeout
|
|
||||||
> | null>(null);
|
|
||||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
|
|
||||||
const handleContentChange = useCallback(
|
const handleContentChange = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
import { ActionIcon, Box, Text, TextInput } from '@mantine/core';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
type MouseEvent,
|
|
||||||
type DragEvent,
|
type DragEvent,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
useState,
|
type MouseEvent,
|
||||||
useRef,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Box, ActionIcon, Text, TextInput } from '@mantine/core';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { type MouseEvent, type DragEvent, useState } from 'react';
|
import { ActionIcon, Box } from '@mantine/core';
|
||||||
import { Box, ActionIcon } from '@mantine/core';
|
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
|
import { type DragEvent, type MouseEvent, 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,10 +10,7 @@ interface NotepadTabsProps {
|
||||||
onCloseTab: (title: string) => void;
|
onCloseTab: (title: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotepadTabs = ({
|
export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) => {
|
||||||
onCreateNewTab,
|
|
||||||
onCloseTab,
|
|
||||||
}: NotepadTabsProps) => {
|
|
||||||
const {
|
const {
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Select as MantineSelect } from '@mantine/core';
|
|
||||||
import type { SelectProps } from '@mantine/core';
|
import type { SelectProps } from '@mantine/core';
|
||||||
|
import { Select as MantineSelect } from '@mantine/core';
|
||||||
|
|
||||||
export const Select = ({
|
export const Select = ({
|
||||||
allowDeselect = false,
|
allowDeselect = false,
|
||||||
|
|
@ -7,10 +7,5 @@ export const Select = ({
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
...props
|
...props
|
||||||
}: SelectProps) => (
|
}: SelectProps) => (
|
||||||
<MantineSelect
|
<MantineSelect allowDeselect={allowDeselect} clearable={clearable} size={size} {...props} />
|
||||||
allowDeselect={allowDeselect}
|
|
||||||
clearable={clearable}
|
|
||||||
size={size}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
import type { ComboboxItem } from '@mantine/core';
|
import type { ComboboxItem } from '@mantine/core';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
||||||
import type { SelectOption } from '@/types';
|
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
|
import type { SelectOption } from '@/types';
|
||||||
|
|
||||||
interface SelectWithTooltipProps {
|
interface SelectWithTooltipProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Group, List, Tooltip } from '@mantine/core';
|
import { Group, List, Tooltip } from '@mantine/core';
|
||||||
import { AlertTriangle, Info } from 'lucide-react';
|
import { AlertTriangle, Info } from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface WarningItem {
|
interface WarningItem {
|
||||||
type: 'warning' | 'info';
|
type: 'warning' | 'info';
|
||||||
|
|
@ -38,11 +38,7 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
||||||
multiline
|
multiline
|
||||||
maw={320}
|
maw={320}
|
||||||
>
|
>
|
||||||
<AlertTriangle
|
<AlertTriangle size={18} color="var(--mantine-color-orange-6)" strokeWidth={2} />
|
||||||
size={18}
|
|
||||||
color="var(--mantine-color-orange-6)"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{infoMessages.length > 0 && (
|
{infoMessages.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
|
||||||
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
|
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 { getPlatformDisplayName } from '@/utils/platform';
|
|
||||||
import { formatDownloadSize } from '@/utils/format';
|
|
||||||
import { getAssetDescription } from '@/utils/assets';
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import type { DownloadItem } from '@/types/electron';
|
import type { DownloadItem } from '@/types/electron';
|
||||||
|
import { getAssetDescription } from '@/utils/assets';
|
||||||
|
import { formatDownloadSize } from '@/utils/format';
|
||||||
|
import { getPlatformDisplayName } from '@/utils/platform';
|
||||||
|
|
||||||
interface DownloadScreenProps {
|
interface DownloadScreenProps {
|
||||||
onDownloadComplete: () => void;
|
onDownloadComplete: () => void;
|
||||||
|
|
@ -73,14 +73,10 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{availableDownloads.map((download) => {
|
{availableDownloads.map((download) => {
|
||||||
const isDownloading =
|
const isDownloading =
|
||||||
Boolean(downloading) &&
|
Boolean(downloading) && downloadingAsset === download.name;
|
||||||
downloadingAsset === download.name;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
||||||
key={download.name}
|
|
||||||
ref={isDownloading ? downloadingItemRef : null}
|
|
||||||
>
|
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
backend={{
|
backend={{
|
||||||
name: download.name,
|
name: download.name,
|
||||||
|
|
@ -95,8 +91,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
description={getAssetDescription(download.name)}
|
description={getAssetDescription(download.name)}
|
||||||
disabled={
|
disabled={
|
||||||
importing ||
|
importing ||
|
||||||
(Boolean(downloading) &&
|
(Boolean(downloading) && downloadingAsset !== download.name)
|
||||||
downloadingAsset !== download.name)
|
|
||||||
}
|
}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -109,9 +104,8 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
Unable to fetch downloads for your platform (
|
Unable to fetch downloads for your platform ({getPlatformDisplayName(platform)}).
|
||||||
{getPlatformDisplayName(platform)}). Check your internet
|
Check your internet connection and try again.
|
||||||
connection and try again.
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Box, Text, Stack } 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 { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { getServerInterfaceInfo } from '@/utils/interface';
|
import { getServerInterfaceInfo } from '@/utils/interface';
|
||||||
import { TITLEBAR_HEIGHT, STATUSBAR_HEIGHT } from '@/constants';
|
|
||||||
|
|
||||||
interface ServerTabProps {
|
interface ServerTabProps {
|
||||||
isServerReady?: boolean;
|
isServerReady?: boolean;
|
||||||
|
|
@ -12,15 +12,10 @@ interface ServerTabProps {
|
||||||
|
|
||||||
export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
|
export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
|
||||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||||
const { frontendPreference, imageGenerationFrontendPreference } =
|
const { frontendPreference, imageGenerationFrontendPreference } = usePreferencesStore();
|
||||||
usePreferencesStore();
|
|
||||||
|
|
||||||
const effectiveImageMode =
|
const effectiveImageMode =
|
||||||
activeTab === 'chat-image'
|
activeTab === 'chat-image' ? true : activeTab === 'chat-text' ? false : isImageGenerationMode;
|
||||||
? true
|
|
||||||
: activeTab === 'chat-text'
|
|
||||||
? false
|
|
||||||
: isImageGenerationMode;
|
|
||||||
|
|
||||||
const { url: iframeUrl, title } = useMemo(
|
const { url: iframeUrl, title } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -48,9 +43,8 @@ export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
|
||||||
Waiting for the server to start...
|
Waiting for the server to start...
|
||||||
</Text>
|
</Text>
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
The{' '}
|
The {title.toLowerCase().includes('ui') ? 'image generation' : 'chat'} interface will
|
||||||
{title.toLowerCase().includes('ui') ? 'image generation' : 'chat'}{' '}
|
load automatically when ready
|
||||||
interface will load automatically when ready
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
import {
|
import { ActionIcon, Box, ScrollArea } from '@mantine/core';
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
forwardRef,
|
|
||||||
useImperativeHandle,
|
|
||||||
} from 'react';
|
|
||||||
import { Box, ScrollArea, ActionIcon } from '@mantine/core';
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
||||||
|
|
||||||
export interface TerminalTabRef {
|
export interface TerminalTabRef {
|
||||||
scrollToBottom: () => void;
|
scrollToBottom: () => void;
|
||||||
|
|
@ -55,7 +49,7 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
const viewport = viewportRef.current;
|
const viewport = viewportRef.current;
|
||||||
viewport.scrollTop = viewport.scrollHeight;
|
viewport.scrollTop = viewport.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [terminalContent, shouldAutoScroll, isUserScrolling]);
|
}, [shouldAutoScroll, isUserScrolling]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
|
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { useRef, useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
||||||
import {
|
import { TerminalTab, type TerminalTabRef } from '@/components/screens/Interface/TerminalTab';
|
||||||
TerminalTab,
|
|
||||||
type TerminalTabRef,
|
|
||||||
} from '@/components/screens/Interface/TerminalTab';
|
|
||||||
import type { InterfaceTab } from '@/types';
|
import type { InterfaceTab } from '@/types';
|
||||||
|
|
||||||
interface InterfaceScreenProps {
|
interface InterfaceScreenProps {
|
||||||
|
|
@ -11,10 +8,7 @@ interface InterfaceScreenProps {
|
||||||
isServerReady: boolean;
|
isServerReady: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InterfaceScreen = ({
|
export const InterfaceScreen = ({ activeTab, isServerReady }: InterfaceScreenProps) => {
|
||||||
activeTab,
|
|
||||||
isServerReady,
|
|
||||||
}: InterfaceScreenProps) => {
|
|
||||||
const terminalTabRef = useRef<TerminalTabRef>(null);
|
const terminalTabRef = useRef<TerminalTabRef>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -35,16 +29,10 @@ export const InterfaceScreen = ({
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display:
|
display: activeTab === 'chat-text' || activeTab === 'chat-image' ? 'block' : 'none',
|
||||||
activeTab === 'chat-text' || activeTab === 'chat-image'
|
|
||||||
? 'block'
|
|
||||||
: 'none',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerTab
|
<ServerTab isServerReady={isServerReady} activeTab={activeTab || undefined} />
|
||||||
isServerReady={isServerReady}
|
|
||||||
activeTab={activeTab || undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
import {
|
import { ActionIcon, Button, Group, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
|
||||||
Stack,
|
|
||||||
Group,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Button,
|
|
||||||
SimpleGrid,
|
|
||||||
ActionIcon,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { useEffect, useState } from 'react';
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
|
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
||||||
|
|
@ -36,16 +28,13 @@ export const AdvancedTab = () => {
|
||||||
|
|
||||||
const handleAddArgument = (newArgument: string) => {
|
const handleAddArgument = (newArgument: string) => {
|
||||||
const currentArgs = additionalArguments.trim();
|
const currentArgs = additionalArguments.trim();
|
||||||
const updatedArgs = currentArgs
|
const updatedArgs = currentArgs ? `${currentArgs} ${newArgument}` : newArgument;
|
||||||
? `${currentArgs} ${newArgument}`
|
|
||||||
: newArgument;
|
|
||||||
setAdditionalArguments(updatedArgs);
|
setAdditionalArguments(updatedArgs);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const detectAccelerationSupport = async () => {
|
const detectAccelerationSupport = async () => {
|
||||||
const support =
|
const support = await window.electronAPI.kobold.detectAccelerationSupport();
|
||||||
await window.electronAPI.kobold.detectAccelerationSupport();
|
|
||||||
|
|
||||||
if (support) {
|
if (support) {
|
||||||
setBackendSupport({
|
setBackendSupport({
|
||||||
|
|
@ -107,20 +96,14 @@ export const AdvancedTab = () => {
|
||||||
</Text>
|
</Text>
|
||||||
<InfoTooltip label="Additional command line arguments to pass to the binary. Leave this empty if you don't know what they are." />
|
<InfoTooltip label="Additional command line arguments to pass to the binary. Leave this empty if you don't know what they are." />
|
||||||
</Group>
|
</Group>
|
||||||
<Button
|
<Button size="xs" variant="light" onClick={() => setCommandLineModalOpen(true)}>
|
||||||
size="xs"
|
|
||||||
variant="light"
|
|
||||||
onClick={() => setCommandLineModalOpen(true)}
|
|
||||||
>
|
|
||||||
View Available Arguments
|
View Available Arguments
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Additional command line arguments"
|
placeholder="Additional command line arguments"
|
||||||
value={additionalArguments}
|
value={additionalArguments}
|
||||||
onChange={(event) =>
|
onChange={(event) => setAdditionalArguments(event.currentTarget.value)}
|
||||||
setAdditionalArguments(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -149,12 +132,8 @@ export const AdvancedTab = () => {
|
||||||
color="red"
|
color="red"
|
||||||
disabled={preLaunchCommands.length === 1}
|
disabled={preLaunchCommands.length === 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newCommands = preLaunchCommands.filter(
|
const newCommands = preLaunchCommands.filter((_, i) => i !== index);
|
||||||
(_, i) => i !== index
|
setPreLaunchCommands(newCommands.length === 0 ? [''] : newCommands);
|
||||||
);
|
|
||||||
setPreLaunchCommands(
|
|
||||||
newCommands.length === 0 ? [''] : newCommands
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,6 @@
|
||||||
import {
|
import { Accordion, Badge, Button, Code, Group, Stack, Text, TextInput } from '@mantine/core';
|
||||||
Text,
|
|
||||||
Stack,
|
|
||||||
Group,
|
|
||||||
Badge,
|
|
||||||
Accordion,
|
|
||||||
Code,
|
|
||||||
TextInput,
|
|
||||||
Button,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Code as CodeIcon } from 'lucide-react';
|
import { Code as CodeIcon } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface CommandLineArgumentsModalProps {
|
interface CommandLineArgumentsModalProps {
|
||||||
|
|
@ -108,8 +99,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--analyze',
|
flag: '--analyze',
|
||||||
description:
|
description: 'Reads the metadata, weight types and tensor names in any GGUF file.',
|
||||||
'Reads the metadata, weight types and tensor names in any GGUF file.',
|
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
|
|
@ -130,18 +120,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
description:
|
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.',
|
'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',
|
type: 'int',
|
||||||
choices: [
|
choices: ['-1', '16', '32', '64', '128', '256', '512', '1024', '2048', '4096'],
|
||||||
'-1',
|
|
||||||
'16',
|
|
||||||
'32',
|
|
||||||
'64',
|
|
||||||
'128',
|
|
||||||
'256',
|
|
||||||
'512',
|
|
||||||
'1024',
|
|
||||||
'2048',
|
|
||||||
'4096',
|
|
||||||
],
|
|
||||||
default: 512,
|
default: 512,
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
},
|
},
|
||||||
|
|
@ -212,8 +191,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--onready',
|
flag: '--onready',
|
||||||
description:
|
description: 'An optional shell command to execute after the model has been loaded.',
|
||||||
'An optional shell command to execute after the model has been loaded.',
|
|
||||||
metavar: '[shell command]',
|
metavar: '[shell command]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
|
@ -310,8 +288,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--mmproj',
|
flag: '--mmproj',
|
||||||
description:
|
description: 'Select a multimodal projector file for vision models like LLaVA.',
|
||||||
'Select a multimodal projector file for vision models like LLaVA.',
|
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Multimodal',
|
category: 'Multimodal',
|
||||||
|
|
@ -353,8 +330,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
{
|
{
|
||||||
flag: '--draftgpulayers',
|
flag: '--draftgpulayers',
|
||||||
aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'],
|
aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'],
|
||||||
description:
|
description: 'How many layers to offload to GPU for the draft model (default=full offload)',
|
||||||
'How many layers to offload to GPU for the draft model (default=full offload)',
|
|
||||||
metavar: '[layers]',
|
metavar: '[layers]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
default: 999,
|
default: 999,
|
||||||
|
|
@ -401,8 +377,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--ignoremissing',
|
flag: '--ignoremissing',
|
||||||
description:
|
description: 'Ignores all missing non-essential files, just skipping them instead.',
|
||||||
'Ignores all missing non-essential files, just skipping them instead.',
|
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
},
|
},
|
||||||
|
|
@ -438,8 +413,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--unpack',
|
flag: '--unpack',
|
||||||
description:
|
description: 'Extracts the file contents of the KoboldCpp binary into a target directory.',
|
||||||
'Extracts the file contents of the KoboldCpp binary into a target directory.',
|
|
||||||
metavar: 'destination',
|
metavar: 'destination',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
|
@ -447,8 +421,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--exportconfig',
|
flag: '--exportconfig',
|
||||||
description:
|
description: 'Exports the current selected arguments as a .kcpps settings file',
|
||||||
'Exports the current selected arguments as a .kcpps settings file',
|
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
|
@ -456,8 +429,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--exporttemplate',
|
flag: '--exporttemplate',
|
||||||
description:
|
description: 'Exports the current selected arguments as a .kcppt template file',
|
||||||
'Exports the current selected arguments as a .kcppt template file',
|
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
|
@ -465,8 +437,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--nomodel',
|
flag: '--nomodel',
|
||||||
description:
|
description: 'Allows you to launch the GUI alone, without selecting any model.',
|
||||||
'Allows you to launch the GUI alone, without selecting any model.',
|
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Advanced',
|
category: 'Advanced',
|
||||||
},
|
},
|
||||||
|
|
@ -577,8 +548,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdquant',
|
flag: '--sdquant',
|
||||||
description:
|
description: 'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
|
||||||
'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
|
|
||||||
metavar: '[quantization level 0/1/2]',
|
metavar: '[quantization level 0/1/2]',
|
||||||
type: 'int',
|
type: 'int',
|
||||||
choices: ['0', '1', '2'],
|
choices: ['0', '1', '2'],
|
||||||
|
|
@ -605,8 +575,7 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdvaeauto',
|
flag: '--sdvaeauto',
|
||||||
description:
|
description: 'Uses a built-in VAE via TAE SD, which is very fast and fixed bad VAEs.',
|
||||||
'Uses a built-in VAE via TAE SD, which is very fast and fixed bad VAEs.',
|
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Image Generation',
|
category: 'Image Generation',
|
||||||
},
|
},
|
||||||
|
|
@ -629,23 +598,20 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdoffloadcpu',
|
flag: '--sdoffloadcpu',
|
||||||
description:
|
description: 'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
|
||||||
'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
|
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Image Generation',
|
category: 'Image Generation',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--sdgendefaults',
|
flag: '--sdgendefaults',
|
||||||
description:
|
description: 'Sets default parameters for image generation, as a JSON string.',
|
||||||
'Sets default parameters for image generation, as a JSON string.',
|
|
||||||
metavar: '{"parameter":"value",...}',
|
metavar: '{"parameter":"value",...}',
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Image Generation',
|
category: 'Image Generation',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--whispermodel',
|
flag: '--whispermodel',
|
||||||
description:
|
description: 'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
|
||||||
'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
|
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Audio',
|
category: 'Audio',
|
||||||
|
|
@ -688,16 +654,14 @@ const COMMAND_LINE_ARGUMENTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--embeddingsmodel',
|
flag: '--embeddingsmodel',
|
||||||
description:
|
description: 'Specify an embeddings model to be loaded for generating embedding vectors.',
|
||||||
'Specify an embeddings model to be loaded for generating embedding vectors.',
|
|
||||||
metavar: '[filename]',
|
metavar: '[filename]',
|
||||||
default: '',
|
default: '',
|
||||||
category: 'Embeddings',
|
category: 'Embeddings',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
flag: '--embeddingsgpu',
|
flag: '--embeddingsgpu',
|
||||||
description:
|
description: 'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
|
||||||
'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
|
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
category: 'Embeddings',
|
category: 'Embeddings',
|
||||||
},
|
},
|
||||||
|
|
@ -740,10 +704,7 @@ export const CommandLineArgumentsModal = ({
|
||||||
(arg) =>
|
(arg) =>
|
||||||
arg.flag.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
arg.flag.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
arg.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
arg.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
(arg.aliases &&
|
arg.aliases?.some((alias) => alias.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
arg.aliases.some((alias) =>
|
|
||||||
alias.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filteredArgs.length > 0) {
|
if (filteredArgs.length > 0) {
|
||||||
|
|
@ -782,8 +743,8 @@ export const CommandLineArgumentsModal = ({
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
These are additional command line arguments that can be added to the
|
These are additional command line arguments that can be added to the "Additional
|
||||||
"Additional Arguments" input field.
|
Arguments" input field.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|
@ -816,18 +777,9 @@ export const CommandLineArgumentsModal = ({
|
||||||
>
|
>
|
||||||
<Group gap="xs" wrap="wrap" justify="space-between">
|
<Group gap="xs" wrap="wrap" justify="space-between">
|
||||||
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
|
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
|
||||||
<Code
|
<Code style={{ fontSize: '0.875em', fontWeight: 600 }}>{arg.flag}</Code>
|
||||||
style={{ fontSize: '0.875em', fontWeight: 600 }}
|
{arg.aliases?.map((alias) => (
|
||||||
>
|
<Code key={alias} style={{ fontSize: '0.75em' }} c="dimmed">
|
||||||
{arg.flag}
|
|
||||||
</Code>
|
|
||||||
{arg.aliases &&
|
|
||||||
arg.aliases.map((alias) => (
|
|
||||||
<Code
|
|
||||||
key={alias}
|
|
||||||
style={{ fontSize: '0.75em' }}
|
|
||||||
c="dimmed"
|
|
||||||
>
|
|
||||||
{alias}
|
{alias}
|
||||||
</Code>
|
</Code>
|
||||||
))}
|
))}
|
||||||
|
|
@ -839,11 +791,7 @@ export const CommandLineArgumentsModal = ({
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{onAddArgument && (
|
{onAddArgument && (
|
||||||
<Button
|
<Button size="xs" variant="light" onClick={() => handleAddArgument(arg)}>
|
||||||
size="xs"
|
|
||||||
variant="light"
|
|
||||||
onClick={() => handleAddArgument(arg)}
|
|
||||||
>
|
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -853,9 +801,7 @@ export const CommandLineArgumentsModal = ({
|
||||||
{arg.description}
|
{arg.description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{(arg.metavar ||
|
{(arg.metavar || arg.default !== undefined || arg.choices) && (
|
||||||
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,11 +1,11 @@
|
||||||
import { Stack, Text, Group, Button, Tooltip } from '@mantine/core';
|
import { Button, Group, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { useState, useCallback } from 'react';
|
import { Check, File, Plus, Save, Trash2 } from 'lucide-react';
|
||||||
import { Save, File, Plus, Check, Trash2 } from 'lucide-react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import { Select } from '@/components/Select';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
|
import { stripFileExtension } from '@/utils/format';
|
||||||
import { CreateConfigModal } from './CreateConfigModal';
|
import { CreateConfigModal } from './CreateConfigModal';
|
||||||
import { DeleteConfigModal } from './DeleteConfigModal';
|
import { DeleteConfigModal } from './DeleteConfigModal';
|
||||||
import { Select } from '@/components/Select';
|
|
||||||
import { stripFileExtension } from '@/utils/format';
|
|
||||||
|
|
||||||
interface ConfigFileManagerProps {
|
interface ConfigFileManagerProps {
|
||||||
configFiles: ConfigFile[];
|
configFiles: ConfigFile[];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { TextInput, Group, Button, Stack } from '@mantine/core';
|
import { Button, Group, Stack, TextInput } from '@mantine/core';
|
||||||
import { Modal } from '@/components/Modal';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
interface CreateConfigModalProps {
|
interface CreateConfigModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -19,8 +19,7 @@ export const CreateConfigModal = ({
|
||||||
|
|
||||||
const trimmedConfigName = newConfigName.trim();
|
const trimmedConfigName = newConfigName.trim();
|
||||||
const configNameExists =
|
const configNameExists =
|
||||||
trimmedConfigName &&
|
trimmedConfigName && existingConfigNames.includes(trimmedConfigName.toLowerCase());
|
||||||
existingConfigNames.includes(trimmedConfigName.toLowerCase());
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setNewConfigName('');
|
setNewConfigName('');
|
||||||
|
|
@ -35,12 +34,7 @@ export const CreateConfigModal = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal opened={opened} onClose={handleClose} title="Create New Configuration" size="sm">
|
||||||
opened={opened}
|
|
||||||
onClose={handleClose}
|
|
||||||
title="Create New Configuration"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name"
|
label="Name"
|
||||||
|
|
@ -48,11 +42,7 @@ export const CreateConfigModal = ({
|
||||||
value={newConfigName}
|
value={newConfigName}
|
||||||
onChange={(event) => setNewConfigName(event.currentTarget.value)}
|
onChange={(event) => setNewConfigName(event.currentTarget.value)}
|
||||||
data-autofocus
|
data-autofocus
|
||||||
error={
|
error={configNameExists ? 'A configuration with this name already exists' : undefined}
|
||||||
configNameExists
|
|
||||||
? 'A configuration with this name already exists'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end" gap="sm">
|
<Group justify="flex-end" gap="sm">
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<Button variant="outline" onClick={handleClose}>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Text, Group, Button, Stack } 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';
|
||||||
|
|
||||||
|
|
@ -21,8 +21,7 @@ export const DeleteConfigModal = ({
|
||||||
<Modal opened={opened} onClose={onClose} title="Delete Configuration">
|
<Modal opened={opened} onClose={onClose} title="Delete Configuration">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Are you sure you want to delete “{displayName}”? This
|
Are you sure you want to delete “{displayName}”? This action cannot be undone.
|
||||||
action cannot be undone.
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Group justify="flex-end" gap="sm">
|
<Group justify="flex-end" gap="sm">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Text, Group, Badge, Box } from '@mantine/core';
|
import { Badge, Box, Group, Text } from '@mantine/core';
|
||||||
import type { AccelerationOption } from '@/types';
|
import type { AccelerationOption } from '@/types';
|
||||||
import { GPUDevice } from '@/types/hardware';
|
import type { GPUDevice } from '@/types/hardware';
|
||||||
|
|
||||||
type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
|
type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
|
||||||
|
|
||||||
|
|
@ -11,9 +11,7 @@ export const AccelerationSelectItem = ({
|
||||||
}: AccelerationSelectItemProps) => {
|
}: AccelerationSelectItemProps) => {
|
||||||
const renderDeviceName = (device: string | GPUDevice) => {
|
const renderDeviceName = (device: string | GPUDevice) => {
|
||||||
const deviceName = typeof device === 'string' ? device : device.name;
|
const deviceName = typeof device === 'string' ? device : device.name;
|
||||||
return deviceName.length > 25
|
return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` : deviceName;
|
||||||
? `${deviceName.slice(0, 25)}...`
|
|
||||||
: deviceName;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Text, Group, Checkbox, TextInput } from '@mantine/core';
|
import { Checkbox, Group, Text, TextInput } from '@mantine/core';
|
||||||
import { useState, useEffect, useRef } 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 { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import type { Acceleration, AccelerationOption } from '@/types';
|
import type { Acceleration, AccelerationOption } from '@/types';
|
||||||
import { Select } from '@/components/Select';
|
|
||||||
|
|
||||||
export const AccelerationSelector = () => {
|
export const AccelerationSelector = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -21,9 +21,7 @@ export const AccelerationSelector = () => {
|
||||||
setAutoGpuLayers,
|
setAutoGpuLayers,
|
||||||
} = useLaunchConfigStore();
|
} = useLaunchConfigStore();
|
||||||
|
|
||||||
const [availableAccelerations, setAvailableAccelerations] = useState<
|
const [availableAccelerations, setAvailableAccelerations] = useState<AccelerationOption[]>([]);
|
||||||
AccelerationOption[]
|
|
||||||
>([]);
|
|
||||||
const [isLoadingAccelerations, setIsLoadingAccelerations] = useState(false);
|
const [isLoadingAccelerations, setIsLoadingAccelerations] = useState(false);
|
||||||
const [isCalculatingLayers, setIsCalculatingLayers] = useState(false);
|
const [isCalculatingLayers, setIsCalculatingLayers] = useState(false);
|
||||||
const [isMac, setIsMac] = useState(false);
|
const [isMac, setIsMac] = useState(false);
|
||||||
|
|
@ -63,9 +61,7 @@ export const AccelerationSelector = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAccelerationAvailable) {
|
if (!isAccelerationAvailable) {
|
||||||
const fallbackAcceleration = availableAccelerations.find(
|
const fallbackAcceleration = availableAccelerations.find((a) => !a.disabled);
|
||||||
(a) => !a.disabled
|
|
||||||
);
|
|
||||||
if (fallbackAcceleration) {
|
if (fallbackAcceleration) {
|
||||||
setAcceleration(fallbackAcceleration.value as Acceleration);
|
setAcceleration(fallbackAcceleration.value as Acceleration);
|
||||||
}
|
}
|
||||||
|
|
@ -76,13 +72,7 @@ export const AccelerationSelector = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const calculateLayers = async () => {
|
const calculateLayers = async () => {
|
||||||
const isCpuOnly = acceleration === 'cpu' && !isMac;
|
const isCpuOnly = acceleration === 'cpu' && !isMac;
|
||||||
if (
|
if (!autoGpuLayers || !model || !contextSize || isCpuOnly || isLoadingAccelerations) {
|
||||||
!autoGpuLayers ||
|
|
||||||
!model ||
|
|
||||||
!contextSize ||
|
|
||||||
isCpuOnly ||
|
|
||||||
isLoadingAccelerations
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,18 +87,13 @@ export const AccelerationSelector = () => {
|
||||||
const selectedDeviceIndices = gpuDeviceSelection
|
const selectedDeviceIndices = gpuDeviceSelection
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((d) => parseInt(d.trim(), 10))
|
.map((d) => parseInt(d.trim(), 10))
|
||||||
.filter((d) => !isNaN(d));
|
.filter((d) => !Number.isNaN(d));
|
||||||
|
|
||||||
const availableVramGB = selectedDeviceIndices.reduce(
|
const availableVramGB = selectedDeviceIndices.reduce((total, deviceIndex) => {
|
||||||
(total, deviceIndex) => {
|
|
||||||
const device = gpuMemory[deviceIndex];
|
const device = gpuMemory[deviceIndex];
|
||||||
const vramGB = device?.totalMemoryGB
|
const vramGB = device?.totalMemoryGB ? parseFloat(device.totalMemoryGB) : 0;
|
||||||
? parseFloat(device.totalMemoryGB)
|
|
||||||
: 0;
|
|
||||||
return total + vramGB;
|
return total + vramGB;
|
||||||
},
|
}, 0);
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (availableVramGB === 0) {
|
if (availableVramGB === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -124,10 +109,7 @@ export const AccelerationSelector = () => {
|
||||||
|
|
||||||
setGpuLayers(result.recommendedLayers);
|
setGpuLayers(result.recommendedLayers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electronAPI.logs.logError(
|
window.electronAPI.logs.logError('Failed to calculate optimal GPU layers', error as Error);
|
||||||
'Failed to calculate optimal GPU layers',
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsCalculatingLayers(false);
|
setIsCalculatingLayers(false);
|
||||||
}
|
}
|
||||||
|
|
@ -158,14 +140,10 @@ export const AccelerationSelector = () => {
|
||||||
</Group>
|
</Group>
|
||||||
<Select
|
<Select
|
||||||
placeholder={
|
placeholder={
|
||||||
isLoadingAccelerations
|
isLoadingAccelerations ? 'Loading accelerations...' : 'Select acceleration'
|
||||||
? 'Loading accelerations...'
|
|
||||||
: 'Select acceleration'
|
|
||||||
}
|
}
|
||||||
value={
|
value={
|
||||||
availableAccelerations.some(
|
availableAccelerations.some((a) => a.value === acceleration && !a.disabled)
|
||||||
(a) => a.value === acceleration && !a.disabled
|
|
||||||
)
|
|
||||||
? acceleration
|
? acceleration
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
@ -179,13 +157,9 @@ export const AccelerationSelector = () => {
|
||||||
label: a.label,
|
label: a.label,
|
||||||
disabled: a.disabled,
|
disabled: a.disabled,
|
||||||
}))}
|
}))}
|
||||||
disabled={
|
disabled={isLoadingAccelerations || availableAccelerations.length === 0}
|
||||||
isLoadingAccelerations || availableAccelerations.length === 0
|
|
||||||
}
|
|
||||||
renderOption={({ option }) => {
|
renderOption={({ option }) => {
|
||||||
const accelerationData = availableAccelerations.find(
|
const accelerationData = availableAccelerations.find((a) => a.value === option.value);
|
||||||
(a) => a.value === option.value
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccelerationSelectItem
|
<AccelerationSelectItem
|
||||||
|
|
@ -215,9 +189,7 @@ export const AccelerationSelector = () => {
|
||||||
: gpuLayers.toString()
|
: gpuLayers.toString()
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onChange={(event) =>
|
onChange={(event) => setGpuLayers(Number(event.target.value) || 0)}
|
||||||
setGpuLayers(Number(event.target.value) || 0)
|
|
||||||
}
|
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
|
|
@ -230,9 +202,7 @@ export const AccelerationSelector = () => {
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Auto"
|
label="Auto"
|
||||||
checked={autoGpuLayers}
|
checked={autoGpuLayers}
|
||||||
onChange={(event) =>
|
onChange={(event) => setAutoGpuLayers(event.currentTarget.checked)}
|
||||||
setAutoGpuLayers(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={acceleration === 'cpu' && !isMac}
|
disabled={acceleration === 'cpu' && !isMac}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Text, Group, TextInput } from '@mantine/core';
|
import { Group, Text, TextInput } from '@mantine/core';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
|
||||||
import { Select } from '@/components/Select';
|
import { Select } from '@/components/Select';
|
||||||
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import type { AccelerationOption } from '@/types';
|
import type { AccelerationOption } from '@/types';
|
||||||
|
|
||||||
const GPU_ACCELERATIONS = ['cuda', 'rocm', 'vulkan', 'clblast'];
|
const GPU_ACCELERATIONS = ['cuda', 'rocm', 'vulkan', 'clblast'];
|
||||||
|
|
@ -11,29 +11,16 @@ interface GpuDeviceSelectorProps {
|
||||||
availableAccelerations: AccelerationOption[];
|
availableAccelerations: AccelerationOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GpuDeviceSelector = ({
|
export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorProps) => {
|
||||||
availableAccelerations,
|
const { acceleration, gpuDeviceSelection, tensorSplit, setGpuDeviceSelection, setTensorSplit } =
|
||||||
}: GpuDeviceSelectorProps) => {
|
useLaunchConfigStore();
|
||||||
const {
|
|
||||||
acceleration,
|
|
||||||
gpuDeviceSelection,
|
|
||||||
tensorSplit,
|
|
||||||
setGpuDeviceSelection,
|
|
||||||
setTensorSplit,
|
|
||||||
} = useLaunchConfigStore();
|
|
||||||
|
|
||||||
const selectedAcceleration = availableAccelerations.find(
|
const selectedAcceleration = availableAccelerations.find((a) => a.value === acceleration);
|
||||||
(a) => a.value === acceleration
|
|
||||||
);
|
|
||||||
const isGpuAcceleration = GPU_ACCELERATIONS.includes(acceleration);
|
const isGpuAcceleration = GPU_ACCELERATIONS.includes(acceleration);
|
||||||
|
|
||||||
const getDiscreteDeviceCount = () => {
|
const getDiscreteDeviceCount = () => {
|
||||||
if (!selectedAcceleration?.devices) return 0;
|
if (!selectedAcceleration?.devices) return 0;
|
||||||
if (
|
if (acceleration === 'clblast' || acceleration === 'vulkan' || acceleration === 'rocm') {
|
||||||
acceleration === 'clblast' ||
|
|
||||||
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;
|
||||||
|
|
@ -68,9 +55,7 @@ export const GpuDeviceSelector = ({
|
||||||
label: `GPU ${index}: ${deviceName}`,
|
label: `GPU ${index}: ${deviceName}`,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(
|
.filter((option): option is NonNullable<typeof option> => option !== null);
|
||||||
(option): option is NonNullable<typeof option> => option !== null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (acceleration === 'vulkan' || acceleration === 'rocm') {
|
if (acceleration === 'vulkan' || acceleration === 'rocm') {
|
||||||
|
|
@ -85,9 +70,7 @@ export const GpuDeviceSelector = ({
|
||||||
label: `GPU ${index}: ${deviceName}`,
|
label: `GPU ${index}: ${deviceName}`,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(
|
.filter((option): option is NonNullable<typeof option> => option !== null);
|
||||||
(option): option is NonNullable<typeof option> => option !== null
|
|
||||||
);
|
|
||||||
|
|
||||||
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions];
|
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
import {
|
import { Group, Slider, Stack, Text, TextInput, Transition } from '@mantine/core';
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
TextInput,
|
|
||||||
Slider,
|
|
||||||
Transition,
|
|
||||||
} from '@mantine/core';
|
|
||||||
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';
|
||||||
|
|
@ -20,12 +13,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
useLaunchConfigStore();
|
useLaunchConfigStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition mounted={configLoaded} transition="fade" duration={100} timingFunction="ease-out">
|
||||||
mounted={configLoaded}
|
|
||||||
transition="fade"
|
|
||||||
duration={100}
|
|
||||||
timingFunction="ease-out"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
{(styles) => (
|
||||||
<Stack gap="md" style={styles}>
|
<Stack gap="md" style={styles}>
|
||||||
<AccelerationSelector />
|
<AccelerationSelector />
|
||||||
|
|
@ -56,9 +44,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
|
||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={contextSize?.toString() || ''}
|
value={contextSize?.toString() || ''}
|
||||||
onChange={(event) =>
|
onChange={(event) => setContextSizeWithStep(Number(event.target.value) || 256)}
|
||||||
setContextSizeWithStep(Number(event.target.value) || 256)
|
|
||||||
}
|
|
||||||
type="number"
|
type="number"
|
||||||
min={256}
|
min={256}
|
||||||
max={131072}
|
max={131072}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Table, Text, Group, Loader } from '@mantine/core';
|
import { Group, Loader, Table, Text } from '@mantine/core';
|
||||||
import { formatBytes } from '@/utils/format';
|
|
||||||
import type { HuggingFaceFileInfo } from '@/types';
|
import type { HuggingFaceFileInfo } from '@/types';
|
||||||
|
import { formatBytes } from '@/utils/format';
|
||||||
|
|
||||||
interface FilesTableProps {
|
interface FilesTableProps {
|
||||||
files: HuggingFaceFileInfo[];
|
files: HuggingFaceFileInfo[];
|
||||||
|
|
@ -35,11 +35,7 @@ export const FilesTable = ({ files, loading, onSelect }: FilesTableProps) => {
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<Table.Tr
|
<Table.Tr key={file.path} onClick={() => onSelect(file)} style={{ cursor: 'pointer' }}>
|
||||||
key={file.path}
|
|
||||||
onClick={() => onSelect(file)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text size="sm" lineClamp={1}>
|
<Text size="sm" lineClamp={1}>
|
||||||
{file.path}
|
{file.path}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { Accordion, Group, Loader, Paper, Text } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Accordion, Paper, Group, Loader, Text } from '@mantine/core';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
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 { HUGGINGFACE_BASE_URL } from '@/constants';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
interface ModelCardProps {
|
interface ModelCardProps {
|
||||||
|
|
@ -20,9 +20,7 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`${HUGGINGFACE_BASE_URL}/${modelId}/raw/main/README.md`);
|
||||||
`${HUGGINGFACE_BASE_URL}/${modelId}/raw/main/README.md`
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('README not available');
|
throw new Error('README not available');
|
||||||
}
|
}
|
||||||
|
|
@ -42,9 +40,7 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
return (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<Accordion.Item value="readme">
|
<Accordion.Item value="readme">
|
||||||
<Accordion.Control onClick={() => void loadReadme()}>
|
<Accordion.Control onClick={() => void loadReadme()}>Model Card</Accordion.Control>
|
||||||
Model Card
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
{loading && (
|
{loading && (
|
||||||
<Group justify="center" py="md">
|
<Group justify="center" py="md">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
import {
|
import { Badge, Group, Loader, Stack, Table, Text, Tooltip } from '@mantine/core';
|
||||||
Table,
|
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
Loader,
|
|
||||||
Stack,
|
|
||||||
Badge,
|
|
||||||
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 { formatDownloads, formatDate } from '@/utils/format';
|
|
||||||
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
|
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
|
||||||
|
import { formatDate, formatDownloads } from '@/utils/format';
|
||||||
|
|
||||||
interface ModelsTableProps {
|
interface ModelsTableProps {
|
||||||
models: HuggingFaceModelInfo[];
|
models: HuggingFaceModelInfo[];
|
||||||
|
|
@ -55,10 +47,7 @@ export const ModelsTable = ({
|
||||||
>
|
>
|
||||||
Downloads{sortBy === 'downloads' && ' ↓'}
|
Downloads{sortBy === 'downloads' && ' ↓'}
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
<Table.Th
|
<Table.Th style={{ width: 90, cursor: 'pointer' }} onClick={() => onSortChange('likes')}>
|
||||||
style={{ width: 90, cursor: 'pointer' }}
|
|
||||||
onClick={() => onSortChange('likes')}
|
|
||||||
>
|
|
||||||
Likes{sortBy === 'likes' && ' ↓'}
|
Likes{sortBy === 'likes' && ' ↓'}
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
|
@ -69,9 +58,7 @@ export const ModelsTable = ({
|
||||||
key={model.id}
|
key={model.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (model.gated) {
|
if (model.gated) {
|
||||||
void window.electronAPI.app.openExternal(
|
void window.electronAPI.app.openExternal(`${HUGGINGFACE_BASE_URL}/${model.id}`);
|
||||||
`${HUGGINGFACE_BASE_URL}/${model.id}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
onSelect(model);
|
onSelect(model);
|
||||||
}
|
}
|
||||||
|
|
@ -88,11 +75,7 @@ export const ModelsTable = ({
|
||||||
</Text>
|
</Text>
|
||||||
{model.gated && (
|
{model.gated && (
|
||||||
<Tooltip label="Gated model - click to open on HuggingFace">
|
<Tooltip label="Gated model - click to open on HuggingFace">
|
||||||
<Badge
|
<Badge size="xs" color="yellow" leftSection={<Lock size={10} />}>
|
||||||
size="xs"
|
|
||||||
color="yellow"
|
|
||||||
leftSection={<Lock size={10} />}
|
|
||||||
>
|
|
||||||
Gated
|
Gated
|
||||||
</Badge>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
import { useEffect, useState, useRef } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Stack,
|
|
||||||
TextInput,
|
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
|
||||||
Badge,
|
|
||||||
Loader,
|
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { Search, ArrowLeft, ExternalLink } from 'lucide-react';
|
import { ArrowLeft, ExternalLink, Search } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
|
|
||||||
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
|
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
|
||||||
import type {
|
import type {
|
||||||
HuggingFaceModelInfo,
|
|
||||||
HuggingFaceFileInfo,
|
HuggingFaceFileInfo,
|
||||||
HuggingFaceSortOption,
|
HuggingFaceModelInfo,
|
||||||
HuggingFaceSearchParams,
|
HuggingFaceSearchParams,
|
||||||
|
HuggingFaceSortOption,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { ModelsTable } from './ModelsTable';
|
|
||||||
import { FilesTable } from './FilesTable';
|
import { FilesTable } from './FilesTable';
|
||||||
import { ModelCard } from './ModelCard';
|
import { ModelCard } from './ModelCard';
|
||||||
|
import { ModelsTable } from './ModelsTable';
|
||||||
|
|
||||||
interface HuggingFaceSearchModalProps {
|
interface HuggingFaceSearchModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -40,9 +40,7 @@ export const HuggingFaceSearchModal = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
searchParams: initialSearchParams,
|
searchParams: initialSearchParams,
|
||||||
}: HuggingFaceSearchModalProps) => {
|
}: HuggingFaceSearchModalProps) => {
|
||||||
const [searchQuery, setSearchQuery] = useState(
|
const [searchQuery, setSearchQuery] = useState(initialSearchParams.search || '');
|
||||||
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);
|
||||||
|
|
@ -108,17 +106,14 @@ export const HuggingFaceSearchModal = ({
|
||||||
|
|
||||||
const handleOpenExternal = () => {
|
const handleOpenExternal = () => {
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
void window.electronAPI.app.openExternal(
|
void window.electronAPI.app.openExternal(`${HUGGINGFACE_BASE_URL}/${selectedModel.id}`);
|
||||||
`${HUGGINGFACE_BASE_URL}/${selectedModel.id}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFilterBadges = () => {
|
const getFilterBadges = () => {
|
||||||
const badges: string[] = [];
|
const badges: string[] = [];
|
||||||
if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase());
|
if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase());
|
||||||
if (searchParams?.pipelineTag)
|
if (searchParams?.pipelineTag) badges.push(searchParams.pipelineTag.replace('-', ' '));
|
||||||
badges.push(searchParams.pipelineTag.replace('-', ' '));
|
|
||||||
return badges;
|
return badges;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -193,8 +188,7 @@ export const HuggingFaceSearchModal = ({
|
||||||
onScrollPositionChange={({ y }) => {
|
onScrollPositionChange={({ y }) => {
|
||||||
const target = scrollAreaRef.current;
|
const target = scrollAreaRef.current;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const isNearBottom =
|
const isNearBottom = target.scrollHeight - y <= target.clientHeight + 100;
|
||||||
target.scrollHeight - y <= target.clientHeight + 100;
|
|
||||||
if (isNearBottom && hasMore && !loading && !selectedModel) {
|
if (isNearBottom && hasMore && !loading && !selectedModel) {
|
||||||
void loadMoreModels();
|
void loadMoreModels();
|
||||||
}
|
}
|
||||||
|
|
@ -204,11 +198,7 @@ export const HuggingFaceSearchModal = ({
|
||||||
{selectedModel ? (
|
{selectedModel ? (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<ModelCard modelId={selectedModel.id} />
|
<ModelCard modelId={selectedModel.id} />
|
||||||
<FilesTable
|
<FilesTable files={files} loading={loadingFiles} onSelect={handleFileSelect} />
|
||||||
files={files}
|
|
||||||
loading={loadingFiles}
|
|
||||||
onSelect={handleFileSelect}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<ModelsTable
|
<ModelsTable
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Stack, Group } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
|
||||||
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
|
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
|
||||||
|
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
|
||||||
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
|
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
||||||
|
|
@ -129,9 +129,7 @@ export const ImageGenerationTab = () => {
|
||||||
placeholder="Select a PhotoMaker file or enter a direct URL"
|
placeholder="Select a PhotoMaker file or enter a direct URL"
|
||||||
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
|
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
|
||||||
onChange={setSdphotomaker}
|
onChange={setSdphotomaker}
|
||||||
onSelectFile={() =>
|
onSelectFile={() => void selectFile('sdphotomaker', 'Select PhotoMaker Model')}
|
||||||
void selectFile('sdphotomaker', 'Select PhotoMaker Model')
|
|
||||||
}
|
|
||||||
searchParams={{
|
searchParams={{
|
||||||
search: 'photomaker',
|
search: 'photomaker',
|
||||||
filter: 'safetensors',
|
filter: 'safetensors',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Modal } from '@/components/Modal';
|
import { Alert, Group, rem, Stack, Text } from '@mantine/core';
|
||||||
import { Stack, Group, Text, Alert, rem } from '@mantine/core';
|
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
import { Modal } from '@/components/Modal';
|
||||||
import type { ModelAnalysis } from '@/types';
|
import type { ModelAnalysis } from '@/types';
|
||||||
|
|
||||||
interface ModelAnalysisModalProps {
|
interface ModelAnalysisModalProps {
|
||||||
|
|
@ -63,9 +63,7 @@ export const ModelAnalysisModal = ({
|
||||||
|
|
||||||
{analysis && (
|
{analysis && (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{analysis.general.name && (
|
{analysis.general.name && <InfoRow label="Name" value={analysis.general.name} />}
|
||||||
<InfoRow label="Name" value={analysis.general.name} />
|
|
||||||
)}
|
|
||||||
<InfoRow label="Architecture" value={analysis.general.architecture} />
|
<InfoRow label="Architecture" value={analysis.general.architecture} />
|
||||||
{analysis.general.parameterCount && (
|
{analysis.general.parameterCount && (
|
||||||
<InfoRow label="Parameters" value={analysis.general.parameterCount} />
|
<InfoRow label="Parameters" value={analysis.general.parameterCount} />
|
||||||
|
|
@ -73,16 +71,10 @@ export const ModelAnalysisModal = ({
|
||||||
<InfoRow label="File Size" value={analysis.general.fileSize} />
|
<InfoRow label="File Size" value={analysis.general.fileSize} />
|
||||||
|
|
||||||
{analysis.architecture.expertCount && (
|
{analysis.architecture.expertCount && (
|
||||||
<InfoRow
|
<InfoRow label="Expert Count (MoE)" value={analysis.architecture.expertCount} />
|
||||||
label="Expert Count (MoE)"
|
|
||||||
value={analysis.architecture.expertCount}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{analysis.context.maxContextLength && (
|
{analysis.context.maxContextLength && (
|
||||||
<InfoRow
|
<InfoRow label="Max Context Length" value={analysis.context.maxContextLength} />
|
||||||
label="Max Context Length"
|
|
||||||
value={analysis.context.maxContextLength}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{analysis.architecture.layers && (
|
{analysis.architecture.layers && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,20 @@
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Group,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
useCombobox,
|
Group,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
Tooltip,
|
||||||
|
useCombobox,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { File, Search, Info } from 'lucide-react';
|
import { File, Info, Search } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
import { LabelWithTooltip } from '@/components/LabelWithTooltip';
|
||||||
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
|
|
||||||
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
|
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
|
||||||
import { getInputValidationState } from '@/utils/validation';
|
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
|
||||||
|
import type { CachedModel, HuggingFaceSearchParams, ModelAnalysis, ModelParamType } from '@/types';
|
||||||
import { logError } from '@/utils/logger';
|
import { logError } from '@/utils/logger';
|
||||||
import type {
|
import { getInputValidationState } from '@/utils/validation';
|
||||||
ModelAnalysis,
|
|
||||||
ModelParamType,
|
|
||||||
CachedModel,
|
|
||||||
HuggingFaceSearchParams,
|
|
||||||
} from '@/types';
|
|
||||||
|
|
||||||
interface ModelFileFieldProps {
|
interface ModelFileFieldProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -46,9 +41,7 @@ export const ModelFileField = ({
|
||||||
}: ModelFileFieldProps) => {
|
}: ModelFileFieldProps) => {
|
||||||
const validationState = getInputValidationState(value);
|
const validationState = getInputValidationState(value);
|
||||||
const [analysisModalOpened, setAnalysisModalOpened] = useState(false);
|
const [analysisModalOpened, setAnalysisModalOpened] = useState(false);
|
||||||
const [modelAnalysis, setModelAnalysis] = useState<ModelAnalysis | null>(
|
const [modelAnalysis, setModelAnalysis] = useState<ModelAnalysis | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const [analysisLoading, setAnalysisLoading] = useState(false);
|
const [analysisLoading, setAnalysisLoading] = useState(false);
|
||||||
const [analysisError, setAnalysisError] = useState<string>();
|
const [analysisError, setAnalysisError] = useState<string>();
|
||||||
const [cachedModels, setCachedModels] = useState<CachedModel[]>([]);
|
const [cachedModels, setCachedModels] = useState<CachedModel[]>([]);
|
||||||
|
|
@ -58,8 +51,7 @@ export const ModelFileField = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const models =
|
const models = await window.electronAPI.kobold.getLocalModels(paramType);
|
||||||
await window.electronAPI.kobold.getLocalModels(paramType);
|
|
||||||
setCachedModels(models);
|
setCachedModels(models);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Failed to load cached models:', error as Error);
|
logError('Failed to load cached models:', error as Error);
|
||||||
|
|
@ -95,8 +87,7 @@ export const ModelFileField = ({
|
||||||
const analysis = await window.electronAPI.kobold.analyzeModel(value);
|
const analysis = await window.electronAPI.kobold.analyzeModel(value);
|
||||||
setModelAnalysis(analysis);
|
setModelAnalysis(analysis);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : 'Failed to analyze model';
|
||||||
error instanceof Error ? error.message : 'Failed to analyze model';
|
|
||||||
setAnalysisError(errorMessage);
|
setAnalysisError(errorMessage);
|
||||||
logError('Failed to analyze model:', error as Error);
|
logError('Failed to analyze model:', error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -126,9 +117,7 @@ export const ModelFileField = ({
|
||||||
}}
|
}}
|
||||||
onFocus={() => combobox.openDropdown()}
|
onFocus={() => combobox.openDropdown()}
|
||||||
onBlur={() => combobox.closeDropdown()}
|
onBlur={() => combobox.closeDropdown()}
|
||||||
error={
|
error={validationState === 'invalid' ? getHelperText() : undefined}
|
||||||
validationState === 'invalid' ? getHelperText() : undefined
|
|
||||||
}
|
|
||||||
rightSection={<Combobox.Chevron />}
|
rightSection={<Combobox.Chevron />}
|
||||||
rightSectionPointerEvents="none"
|
rightSectionPointerEvents="none"
|
||||||
/>
|
/>
|
||||||
|
|
@ -141,20 +130,12 @@ export const ModelFileField = ({
|
||||||
)}
|
)}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={onSelectFile} variant="light" leftSection={<File size={16} />}>
|
||||||
onClick={onSelectFile}
|
|
||||||
variant="light"
|
|
||||||
leftSection={<File size={16} />}
|
|
||||||
>
|
|
||||||
Browse
|
Browse
|
||||||
</Button>
|
</Button>
|
||||||
{searchParams && (
|
{searchParams && (
|
||||||
<Tooltip label="Search Hugging Face">
|
<Tooltip label="Search Hugging Face">
|
||||||
<ActionIcon
|
<ActionIcon onClick={() => setSearchModalOpened(true)} variant="outline" size="lg">
|
||||||
onClick={() => setSearchModalOpened(true)}
|
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<Search size={16} />
|
<Search size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -172,9 +153,7 @@ export const ModelFileField = ({
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={
|
disabled={validationState === 'neutral' || validationState === 'invalid'}
|
||||||
validationState === 'neutral' || validationState === 'invalid'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Stack, Text, TextInput, Group } from '@mantine/core';
|
import { Group, Stack, Text, TextInput } from '@mantine/core';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
||||||
export const NetworkTab = () => {
|
export const NetworkTab = () => {
|
||||||
|
|
@ -57,7 +57,7 @@ export const NetworkTab = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const numValue = Number(value);
|
const numValue = Number(value);
|
||||||
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
|
if (!Number.isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
|
||||||
setPort(numValue);
|
setPort(numValue);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Stack, Group, Text, NumberInput, SimpleGrid } from '@mantine/core';
|
import { Group, NumberInput, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
|
||||||
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
|
||||||
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
|
|
||||||
export const PerformanceTab = () => {
|
export const PerformanceTab = () => {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
|
import { Button, Card, Container, Group, Stack, Tabs } from '@mantine/core';
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { logError } from '@/utils/logger';
|
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
|
||||||
|
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
|
||||||
|
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
|
||||||
|
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
|
||||||
|
import { PerformanceTab } from '@/components/screens/Launch/PerformanceTab';
|
||||||
|
import { WarningDisplay } from '@/components/WarningDisplay';
|
||||||
|
import { DEFAULT_MODEL_URL } from '@/constants';
|
||||||
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
|
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
|
||||||
import { useWarnings } from '@/hooks/useWarnings';
|
import { useWarnings } from '@/hooks/useWarnings';
|
||||||
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
|
|
||||||
import { PerformanceTab } from '@/components/screens/Launch/PerformanceTab';
|
|
||||||
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
|
|
||||||
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
|
|
||||||
import { WarningDisplay } from '@/components/WarningDisplay';
|
|
||||||
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
|
|
||||||
import { DEFAULT_MODEL_URL } from '@/constants';
|
|
||||||
import type { Acceleration, ConfigFile } from '@/types';
|
import type { Acceleration, ConfigFile } from '@/types';
|
||||||
|
import { logError } from '@/utils/logger';
|
||||||
|
|
||||||
interface LaunchScreenProps {
|
interface LaunchScreenProps {
|
||||||
onLaunch: () => void;
|
onLaunch: () => void;
|
||||||
|
|
@ -86,8 +86,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const setHappyDefaults = useCallback(async () => {
|
const setHappyDefaults = useCallback(async () => {
|
||||||
const accelerations =
|
const accelerations = await window.electronAPI.kobold.getAvailableAccelerations();
|
||||||
await window.electronAPI.kobold.getAvailableAccelerations();
|
|
||||||
|
|
||||||
if (!acceleration && accelerations && accelerations.length > 0) {
|
if (!acceleration && accelerations && accelerations.length > 0) {
|
||||||
setAcceleration(accelerations[0].value as Acceleration);
|
setAcceleration(accelerations[0].value as Acceleration);
|
||||||
|
|
@ -96,11 +95,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
|
|
||||||
const setInitialDefaults = useCallback(
|
const setInitialDefaults = useCallback(
|
||||||
(currentModel: string, currentSdModel: string) => {
|
(currentModel: string, currentSdModel: string) => {
|
||||||
if (
|
if (!defaultsSetRef.current && !currentModel.trim() && !currentSdModel.trim()) {
|
||||||
!defaultsSetRef.current &&
|
|
||||||
!currentModel.trim() &&
|
|
||||||
!currentSdModel.trim()
|
|
||||||
) {
|
|
||||||
setModel(DEFAULT_MODEL_URL);
|
setModel(DEFAULT_MODEL_URL);
|
||||||
defaultsSetRef.current = true;
|
defaultsSetRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
@ -139,14 +134,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigLoaded(true);
|
setConfigLoaded(true);
|
||||||
}, [
|
}, [selectedFile, loadConfigFromFile]);
|
||||||
selectedFile,
|
|
||||||
loadConfigFromFile,
|
|
||||||
setConfigFiles,
|
|
||||||
setInstallDir,
|
|
||||||
setSelectedFile,
|
|
||||||
setConfigLoaded,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleFileSelection = async (fileName: string) => {
|
const handleFileSelection = async (fileName: string) => {
|
||||||
setSelectedFile(fileName);
|
setSelectedFile(fileName);
|
||||||
|
|
@ -211,19 +199,13 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
setSelectedFile(fullConfigName);
|
setSelectedFile(fullConfigName);
|
||||||
await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
|
await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
|
||||||
} else {
|
} else {
|
||||||
logError(
|
logError('Failed to create new configuration', new Error('Save operation failed'));
|
||||||
'Failed to create new configuration',
|
|
||||||
new Error('Save operation failed')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveConfig = async () => {
|
const handleSaveConfig = async () => {
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
logError(
|
logError('No configuration file selected for saving', new Error('Selected file is null'));
|
||||||
'No configuration file selected for saving',
|
|
||||||
new Error('Selected file is null')
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,10 +215,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!saveSuccess) {
|
if (!saveSuccess) {
|
||||||
logError(
|
logError('Failed to save configuration', new Error('Save operation failed'));
|
||||||
'Failed to save configuration',
|
|
||||||
new Error('Save operation failed')
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,8 +223,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteConfig = async (fileName: string) => {
|
const handleDeleteConfig = async (fileName: string) => {
|
||||||
const deleteSuccess =
|
const deleteSuccess = await window.electronAPI.kobold.deleteConfigFile(fileName);
|
||||||
await window.electronAPI.kobold.deleteConfigFile(fileName);
|
|
||||||
|
|
||||||
if (deleteSuccess) {
|
if (deleteSuccess) {
|
||||||
await loadConfigFiles();
|
await loadConfigFiles();
|
||||||
|
|
@ -270,16 +248,13 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
void loadConfigFiles();
|
void loadConfigFiles();
|
||||||
|
|
||||||
const handleInstallDirChange = () => {
|
const handleInstallDirChange = () => {
|
||||||
void loadConfigFiles();
|
void loadConfigFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
|
const cleanup = window.electronAPI.kobold.onInstallDirChanged(handleInstallDirChange);
|
||||||
handleInstallDirChange
|
|
||||||
);
|
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [loadConfigFiles]);
|
}, [loadConfigFiles]);
|
||||||
|
|
@ -368,13 +343,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
return (
|
return (
|
||||||
<Container size="sm" mt="md">
|
<Container size="sm" mt="md">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Card
|
<Card withBorder radius="md" shadow="sm" p="lg" style={{ position: 'relative' }}>
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
p="lg"
|
|
||||||
style={{ position: 'relative' }}
|
|
||||||
>
|
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<ConfigFileManager
|
<ConfigFileManager
|
||||||
configFiles={configFiles}
|
configFiles={configFiles}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
|
||||||
import {
|
import {
|
||||||
Container,
|
|
||||||
Card,
|
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
Text,
|
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
Group,
|
Group,
|
||||||
List,
|
|
||||||
ThemeIcon,
|
|
||||||
Image,
|
Image,
|
||||||
|
List,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import iconUrl from '/icon.png';
|
import iconUrl from '/icon.png';
|
||||||
|
|
||||||
interface WelcomeScreenProps {
|
interface WelcomeScreenProps {
|
||||||
|
|
@ -81,8 +81,8 @@ export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Text size="xs" c="dimmed" ta="center" mt="sm">
|
<Text size="xs" c="dimmed" ta="center" mt="sm">
|
||||||
Hardware acceleration requires appropriate drivers to be manually
|
Hardware acceleration requires appropriate drivers to be manually installed (CUDA for
|
||||||
installed (CUDA for NVIDIA GPUs, ROCm for AMD GPUs, etc.)
|
NVIDIA GPUs, ROCm for AMD GPUs, etc.)
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,14 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { Badge, Button, Card, Center, Group, Image, rem, Stack, Text } from '@mantine/core';
|
||||||
import {
|
|
||||||
Text,
|
|
||||||
Stack,
|
|
||||||
Group,
|
|
||||||
Card,
|
|
||||||
Image,
|
|
||||||
Center,
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
rem,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Github } from 'lucide-react';
|
import { Github } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { GITHUB_API, PRODUCT_NAME } from '@/constants';
|
||||||
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
||||||
import { PRODUCT_NAME, GITHUB_API } from '@/constants';
|
|
||||||
import type { SystemVersionInfo } from '@/types/electron';
|
import type { SystemVersionInfo } from '@/types/electron';
|
||||||
|
|
||||||
import icon from '/icon.png';
|
import icon from '/icon.png';
|
||||||
|
|
||||||
export const AboutTab = () => {
|
export const AboutTab = () => {
|
||||||
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(
|
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -46,8 +34,7 @@ export const AboutTab = () => {
|
||||||
{
|
{
|
||||||
icon: Github,
|
icon: Github,
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
onClick: () =>
|
onClick: () => window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
|
||||||
window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -72,12 +59,7 @@ export const AboutTab = () => {
|
||||||
<Text size="xl" fw={600}>
|
<Text size="xl" fw={600}>
|
||||||
{PRODUCT_NAME}
|
{PRODUCT_NAME}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
<Badge variant="light" color="blue" size="lg" style={{ textTransform: 'none' }}>
|
||||||
variant="light"
|
|
||||||
color="blue"
|
|
||||||
size="lg"
|
|
||||||
style={{ textTransform: 'none' }}
|
|
||||||
>
|
|
||||||
v{versionInfo.appVersion}
|
v{versionInfo.appVersion}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
@ -90,15 +72,9 @@ export const AboutTab = () => {
|
||||||
key={button.label}
|
key={button.label}
|
||||||
variant="light"
|
variant="light"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={
|
leftSection={<button.icon style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<button.icon style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
onClick={() => void button.onClick()}
|
onClick={() => void button.onClick()}
|
||||||
style={
|
style={button.label === 'GitHub' ? { textDecoration: 'none' } : undefined}
|
||||||
button.label === 'GitHub'
|
|
||||||
? { textDecoration: 'none' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{button.label}
|
{button.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -113,10 +89,9 @@ export const AboutTab = () => {
|
||||||
About {PRODUCT_NAME}
|
About {PRODUCT_NAME}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
{PRODUCT_NAME} is a user-friendly desktop application that makes it
|
{PRODUCT_NAME} is a user-friendly desktop application that makes it easy to run large
|
||||||
easy to run large language models locally on your machine. Whether
|
language models locally on your machine. Whether you're looking to chat with AI
|
||||||
you're looking to chat with AI models, generate images, or
|
models, generate images, or explore different interfaces like SillyTavern and Open WebUI,{' '}
|
||||||
explore different interfaces like SillyTavern and Open WebUI,{' '}
|
|
||||||
{PRODUCT_NAME} provides a streamlined experience for local AI.
|
{PRODUCT_NAME} provides a streamlined experience for local AI.
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,25 @@
|
||||||
import {
|
import {
|
||||||
|
Group,
|
||||||
|
type MantineColorScheme,
|
||||||
|
rem,
|
||||||
|
SegmentedControl,
|
||||||
|
Slider,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Group,
|
|
||||||
SegmentedControl,
|
|
||||||
rem,
|
|
||||||
Slider,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
type MantineColorScheme,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { ZOOM } from '@/constants';
|
|
||||||
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
||||||
import {
|
import { ZOOM } from '@/constants';
|
||||||
zoomLevelToPercentage,
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
percentageToZoomLevel,
|
import { isValidZoomPercentage, percentageToZoomLevel, zoomLevelToPercentage } from '@/utils/zoom';
|
||||||
isValidZoomPercentage,
|
|
||||||
} from '@/utils/zoom';
|
|
||||||
|
|
||||||
interface AppearanceTabProps {
|
interface AppearanceTabProps {
|
||||||
isOnInterfaceScreen?: boolean;
|
isOnInterfaceScreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppearanceTab = ({
|
export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProps) => {
|
||||||
isOnInterfaceScreen = false,
|
|
||||||
}: AppearanceTabProps) => {
|
|
||||||
const {
|
const {
|
||||||
rawColorScheme,
|
rawColorScheme,
|
||||||
resolvedColorScheme,
|
resolvedColorScheme,
|
||||||
|
|
@ -34,9 +28,7 @@ export const AppearanceTab = ({
|
||||||
const isDark = resolvedColorScheme === 'dark';
|
const isDark = resolvedColorScheme === 'dark';
|
||||||
|
|
||||||
const [zoomLevel, setZoomLevel] = useState<number>(ZOOM.DEFAULT_LEVEL);
|
const [zoomLevel, setZoomLevel] = useState<number>(ZOOM.DEFAULT_LEVEL);
|
||||||
const [zoomPercentage, setZoomPercentage] = useState(
|
const [zoomPercentage, setZoomPercentage] = useState(ZOOM.DEFAULT_PERCENTAGE.toString());
|
||||||
ZOOM.DEFAULT_PERCENTAGE.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
|
|
@ -67,7 +59,7 @@ export const AppearanceTab = ({
|
||||||
const handleZoomPercentageChange = (value: string) => {
|
const handleZoomPercentageChange = (value: string) => {
|
||||||
setZoomPercentage(value);
|
setZoomPercentage(value);
|
||||||
const numValue = Number(value);
|
const numValue = Number(value);
|
||||||
if (!isNaN(numValue) && isValidZoomPercentage(numValue)) {
|
if (!Number.isNaN(numValue) && isValidZoomPercentage(numValue)) {
|
||||||
const newZoomLevel = percentageToZoomLevel(numValue);
|
const newZoomLevel = percentageToZoomLevel(numValue);
|
||||||
setZoomLevel(newZoomLevel);
|
setZoomLevel(newZoomLevel);
|
||||||
|
|
||||||
|
|
@ -91,17 +83,11 @@ export const AppearanceTab = ({
|
||||||
onChange={handleColorSchemeChange}
|
onChange={handleColorSchemeChange}
|
||||||
styles={(theme) => ({
|
styles={(theme) => ({
|
||||||
root: {
|
root: {
|
||||||
border: `0.5px solid ${
|
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
||||||
isDark ? theme.colors.dark[4] : theme.colors.gray[4]
|
|
||||||
}`,
|
|
||||||
},
|
},
|
||||||
indicator: {
|
indicator: {
|
||||||
backgroundColor: isDark
|
backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
|
||||||
? theme.colors.dark[5]
|
border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
||||||
: theme.colors.gray[2],
|
|
||||||
border: `1px solid ${
|
|
||||||
isDark ? theme.colors.dark[4] : theme.colors.gray[4]
|
|
||||||
}`,
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
data={[
|
data={[
|
||||||
|
|
@ -145,13 +131,11 @@ export const AppearanceTab = ({
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={zoomPercentage}
|
value={zoomPercentage}
|
||||||
onChange={(event) =>
|
onChange={(event) => handleZoomPercentageChange(event.currentTarget.value)}
|
||||||
handleZoomPercentageChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
const value = event.currentTarget.value;
|
const value = event.currentTarget.value;
|
||||||
const numValue = Number(value);
|
const numValue = Number(value);
|
||||||
if (isNaN(numValue) || !isValidZoomPercentage(numValue)) {
|
if (Number.isNaN(numValue) || !isValidZoomPercentage(numValue)) {
|
||||||
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());
|
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,14 @@
|
||||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core';
|
||||||
import {
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Group,
|
|
||||||
Card,
|
|
||||||
Loader,
|
|
||||||
Center,
|
|
||||||
Anchor,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-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 { getAssetDescription } from '@/utils/assets';
|
|
||||||
import {
|
|
||||||
getDisplayNameFromPath,
|
|
||||||
stripAssetExtensions,
|
|
||||||
compareVersions,
|
|
||||||
} from '@/utils/version';
|
|
||||||
import { formatDownloadSize } from '@/utils/format';
|
|
||||||
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron';
|
|
||||||
import type { BackendInfo } from '@/types';
|
import type { BackendInfo } from '@/types';
|
||||||
|
import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron';
|
||||||
|
import { getAssetDescription } from '@/utils/assets';
|
||||||
|
import { formatDownloadSize } from '@/utils/format';
|
||||||
|
import { compareVersions, getDisplayNameFromPath, stripAssetExtensions } from '@/utils/version';
|
||||||
|
|
||||||
export const BackendsTab = () => {
|
export const BackendsTab = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -34,16 +21,10 @@ export const BackendsTab = () => {
|
||||||
refreshDownloads,
|
refreshDownloads,
|
||||||
} = useKoboldBackendsStore();
|
} = useKoboldBackendsStore();
|
||||||
|
|
||||||
const [installedBackends, setInstalledBackends] = useState<
|
const [installedBackends, setInstalledBackends] = useState<InstalledBackend[]>([]);
|
||||||
InstalledBackend[]
|
const [currentBackend, setCurrentBackend] = useState<InstalledBackend | null>(null);
|
||||||
>([]);
|
|
||||||
const [currentBackend, setCurrentBackend] = useState<InstalledBackend | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [loadingInstalled, setLoadingInstalled] = useState(true);
|
const [loadingInstalled, setLoadingInstalled] = useState(true);
|
||||||
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
|
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
const downloadingItemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -94,19 +75,14 @@ export const BackendsTab = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(
|
||||||
installedBackend &&
|
installedBackend && currentBackend && currentBackend.path === installedBackend.path
|
||||||
currentBackend &&
|
|
||||||
currentBackend.path === installedBackend.path
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (installedBackend) {
|
if (installedBackend) {
|
||||||
processedInstalled.add(installedBackend.path);
|
processedInstalled.add(installedBackend.path);
|
||||||
|
|
||||||
const hasUpdate =
|
const hasUpdate =
|
||||||
compareVersions(
|
compareVersions(download.version || 'unknown', installedBackend.version) > 0;
|
||||||
download.version || 'unknown',
|
|
||||||
installedBackend.version
|
|
||||||
) > 0;
|
|
||||||
|
|
||||||
backends.push({
|
backends.push({
|
||||||
name: download.name,
|
name: download.name,
|
||||||
|
|
@ -135,9 +111,7 @@ export const BackendsTab = () => {
|
||||||
installedBackends.forEach((installed) => {
|
installedBackends.forEach((installed) => {
|
||||||
if (!processedInstalled.has(installed.path)) {
|
if (!processedInstalled.has(installed.path)) {
|
||||||
const displayName = getDisplayNameFromPath(installed);
|
const displayName = getDisplayNameFromPath(installed);
|
||||||
const isCurrent = Boolean(
|
const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path);
|
||||||
currentBackend && currentBackend.path === installed.path
|
|
||||||
);
|
|
||||||
|
|
||||||
backends.push({
|
backends.push({
|
||||||
name: displayName,
|
name: displayName,
|
||||||
|
|
@ -212,9 +186,7 @@ export const BackendsTab = () => {
|
||||||
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(
|
const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath);
|
||||||
backend.installedPath
|
|
||||||
);
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadInstalledBackends();
|
await loadInstalledBackends();
|
||||||
}
|
}
|
||||||
|
|
@ -223,9 +195,7 @@ export const BackendsTab = () => {
|
||||||
const makeCurrent = (backend: BackendInfo) => {
|
const makeCurrent = (backend: BackendInfo) => {
|
||||||
if (!backend.installedPath) return;
|
if (!backend.installedPath) return;
|
||||||
|
|
||||||
const targetBackend = installedBackends.find(
|
const targetBackend = installedBackends.find((b) => b.path === backend.installedPath);
|
||||||
(b) => b.path === backend.installedPath
|
|
||||||
);
|
|
||||||
if (targetBackend) {
|
if (targetBackend) {
|
||||||
setCurrentBackend(targetBackend);
|
setCurrentBackend(targetBackend);
|
||||||
}
|
}
|
||||||
|
|
@ -292,11 +262,7 @@ export const BackendsTab = () => {
|
||||||
>
|
>
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
backend={backend}
|
backend={backend}
|
||||||
size={
|
size={backend.size ? formatDownloadSize(backend.size, backend.downloadUrl) : ''}
|
||||||
backend.size
|
|
||||||
? formatDownloadSize(backend.size, backend.downloadUrl)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
description={getAssetDescription(backend.name)}
|
description={getAssetDescription(backend.name)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { Anchor, Box, Button, Group, rem, Stack, Text } from '@mantine/core';
|
||||||
import { Text, Box, Anchor, rem, Button, Group, Stack } from '@mantine/core';
|
import { Image, Monitor } from 'lucide-react';
|
||||||
import { Monitor, Image } from 'lucide-react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import type {
|
|
||||||
FrontendPreference,
|
|
||||||
ImageGenerationFrontendPreference,
|
|
||||||
} from '@/types';
|
|
||||||
import { FRONTENDS } from '@/constants';
|
|
||||||
import { Select } from '@/components/Select';
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
|
import { Select } from '@/components/Select';
|
||||||
|
import { FRONTENDS } from '@/constants';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import type { FrontendPreference, ImageGenerationFrontendPreference } from '@/types';
|
||||||
|
|
||||||
interface FrontendRequirement {
|
interface FrontendRequirement {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -37,9 +34,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
setImageGenerationFrontendPreference,
|
setImageGenerationFrontendPreference,
|
||||||
} = usePreferencesStore();
|
} = usePreferencesStore();
|
||||||
|
|
||||||
const [frontendRequirements, setFrontendRequirements] = useState<
|
const [frontendRequirements, setFrontendRequirements] = useState<Map<string, boolean>>(new Map());
|
||||||
Map<string, boolean>
|
|
||||||
>(new Map());
|
|
||||||
|
|
||||||
const [showClearDataModal, setShowClearDataModal] = useState(false);
|
const [showClearDataModal, setShowClearDataModal] = useState(false);
|
||||||
|
|
||||||
|
|
@ -63,8 +58,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
url: 'https://nodejs.org/',
|
url: 'https://nodejs.org/',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requirementCheck: () =>
|
requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(),
|
||||||
window.electronAPI.dependencies.isNpxAvailable(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'openwebui',
|
value: 'openwebui',
|
||||||
|
|
@ -131,9 +125,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageGenerationFrontendChange = (value: string | null) => {
|
const handleImageGenerationFrontendChange = (value: string | null) => {
|
||||||
setImageGenerationFrontendPreference(
|
setImageGenerationFrontendPreference(value as ImageGenerationFrontendPreference);
|
||||||
value as ImageGenerationFrontendPreference
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearOpenWebUIData = async () => {
|
const handleClearOpenWebUIData = async () => {
|
||||||
|
|
@ -158,13 +150,12 @@ export const FrontendInterfaceSelector = ({
|
||||||
if (!requirementGroups.has(reqKey)) {
|
if (!requirementGroups.has(reqKey)) {
|
||||||
requirementGroups.set(reqKey, []);
|
requirementGroups.set(reqKey, []);
|
||||||
}
|
}
|
||||||
requirementGroups.get(reqKey)!.push(config.label);
|
requirementGroups.get(reqKey)?.push(config.label);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mt="sm">
|
<Box mt="sm">
|
||||||
{Array.from(requirementGroups.entries()).map(
|
{Array.from(requirementGroups.entries()).map(([reqKey, frontendLabels]) => {
|
||||||
([reqKey, frontendLabels]) => {
|
|
||||||
const firstDisabledFrontend = disabledFrontends.find(
|
const firstDisabledFrontend = disabledFrontends.find(
|
||||||
(config) =>
|
(config) =>
|
||||||
getUnmetRequirementsForFrontend(config.value)
|
getUnmetRequirementsForFrontend(config.value)
|
||||||
|
|
@ -189,12 +180,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
{frontendText} {isAre} disabled - requires{' '}
|
{frontendText} {isAre} disabled - requires{' '}
|
||||||
{unmetReqs.map((req, index) => (
|
{unmetReqs.map((req, index) => (
|
||||||
<span key={req.id}>
|
<span key={req.id}>
|
||||||
<Anchor
|
<Anchor href={req.url} target="_blank" rel="noopener noreferrer" td="underline">
|
||||||
href={req.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
td="underline"
|
|
||||||
>
|
|
||||||
{req.name}
|
{req.name}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{index < unmetReqs.length - 1 ? ', ' : ''}
|
{index < unmetReqs.length - 1 ? ', ' : ''}
|
||||||
|
|
@ -202,8 +188,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
))}
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -224,7 +209,6 @@ export const FrontendInterfaceSelector = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (frontendPreference) {
|
if (frontendPreference) {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
void checkAllFrontendRequirements();
|
void checkAllFrontendRequirements();
|
||||||
}
|
}
|
||||||
}, [frontendPreference, checkAllFrontendRequirements]);
|
}, [frontendPreference, checkAllFrontendRequirements]);
|
||||||
|
|
@ -238,8 +222,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
|
|
||||||
{getUnmetRequirements().length === 0 && (
|
{getUnmetRequirements().length === 0 && (
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
Choose which frontend interface to use for interacting with AI
|
Choose which frontend interface to use for interacting with AI models
|
||||||
models
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -274,9 +257,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
label: config.label,
|
label: config.label,
|
||||||
disabled: !isFrontendAvailable(config.value),
|
disabled: !isFrontendAvailable(config.value),
|
||||||
}))}
|
}))}
|
||||||
leftSection={
|
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<Monitor style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -317,10 +298,7 @@ export const FrontendInterfaceSelector = ({
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="flex-end" gap="sm">
|
<Group justify="flex-end" gap="sm">
|
||||||
<Button
|
<Button variant="subtle" onClick={() => setShowClearDataModal(false)}>
|
||||||
variant="subtle"
|
|
||||||
onClick={() => setShowClearDataModal(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onClick={() => void handleClearOpenWebUIData()}>
|
<Button color="red" onClick={() => void handleClearOpenWebUIData()}>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { Stack, Text } from '@mantine/core';
|
import { Stack, Text } from '@mantine/core';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { useEffect, useState } from 'react';
|
||||||
import { Switch } from '@/components/Switch';
|
import { Switch } from '@/components/Switch';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
export const GeneralTab = () => {
|
||||||
const [enableSystemTray, setEnableSystemTray] = useState(false);
|
const [enableSystemTray, setEnableSystemTray] = useState(false);
|
||||||
const [startMinimizedToTray, setStartMinimizedToTray] = useState(false);
|
const [startMinimizedToTray, setStartMinimizedToTray] = useState(false);
|
||||||
const { systemMonitoringEnabled, setSystemMonitoringEnabled } =
|
const { systemMonitoringEnabled, setSystemMonitoringEnabled } = usePreferencesStore();
|
||||||
usePreferencesStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSystemTrayPreference = async () => {
|
const loadSystemTrayPreference = async () => {
|
||||||
|
|
@ -48,9 +47,7 @@ export const GeneralTab = () => {
|
||||||
<Switch
|
<Switch
|
||||||
label="Show system metrics"
|
label="Show system metrics"
|
||||||
checked={systemMonitoringEnabled}
|
checked={systemMonitoringEnabled}
|
||||||
onChange={(event) =>
|
onChange={(event) => setSystemMonitoringEnabled(event.currentTarget.checked)}
|
||||||
setSystemMonitoringEnabled(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -59,15 +56,12 @@ export const GeneralTab = () => {
|
||||||
System Tray
|
System Tray
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
Add a system tray icon with quick access to launch, eject and monitor
|
Add a system tray icon with quick access to launch, eject and monitor system metrics
|
||||||
system metrics
|
|
||||||
</Text>
|
</Text>
|
||||||
<Switch
|
<Switch
|
||||||
label="Enable system tray icon"
|
label="Enable system tray icon"
|
||||||
checked={enableSystemTray}
|
checked={enableSystemTray}
|
||||||
onChange={(event) =>
|
onChange={(event) => void handleSystemTrayToggle(event.currentTarget.checked)}
|
||||||
void handleSystemTrayToggle(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -82,9 +76,7 @@ export const GeneralTab = () => {
|
||||||
label="Start minimized to tray"
|
label="Start minimized to tray"
|
||||||
checked={startMinimizedToTray}
|
checked={startMinimizedToTray}
|
||||||
disabled={!enableSystemTray}
|
disabled={!enableSystemTray}
|
||||||
onChange={(event) =>
|
onChange={(event) => void handleStartMinimizedToggle(event.currentTarget.checked)}
|
||||||
void handleStartMinimizedToggle(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { Group, rem, Tabs, Text } from '@mantine/core';
|
||||||
import { Tabs, Text, Group, rem } from '@mantine/core';
|
|
||||||
import {
|
import {
|
||||||
Settings,
|
|
||||||
Palette,
|
|
||||||
SlidersHorizontal,
|
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Monitor,
|
|
||||||
Info,
|
Info,
|
||||||
|
Monitor,
|
||||||
|
Palette,
|
||||||
|
Settings,
|
||||||
|
SlidersHorizontal,
|
||||||
Wrench,
|
Wrench,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GeneralTab } from '@/components/settings/GeneralTab';
|
import { useEffect, useState } from 'react';
|
||||||
import { BackendsTab } from '@/components/settings/BackendsTab';
|
import { Modal } from '@/components/Modal';
|
||||||
|
import { AboutTab } from '@/components/settings/AboutTab';
|
||||||
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
||||||
|
import { BackendsTab } from '@/components/settings/BackendsTab';
|
||||||
|
import { GeneralTab } from '@/components/settings/GeneralTab';
|
||||||
import { SystemTab } from '@/components/settings/SystemTab';
|
import { SystemTab } from '@/components/settings/SystemTab';
|
||||||
import { TroubleshootingTab } from '@/components/settings/TroubleshootingTab';
|
import { TroubleshootingTab } from '@/components/settings/TroubleshootingTab';
|
||||||
import { AboutTab } from '@/components/settings/AboutTab';
|
|
||||||
import type { Screen } from '@/types';
|
import type { Screen } from '@/types';
|
||||||
import { Modal } from '@/components/Modal';
|
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -33,17 +33,14 @@ export const SettingsModal = ({
|
||||||
}: SettingsModalProps) => {
|
}: SettingsModalProps) => {
|
||||||
const [activeTab, setActiveTab] = useState('general');
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
|
|
||||||
const showBackendsTab =
|
const showBackendsTab = currentScreen !== 'download' && currentScreen !== 'welcome';
|
||||||
currentScreen !== 'download' && currentScreen !== 'welcome';
|
|
||||||
|
|
||||||
const effectiveActiveTab =
|
const effectiveActiveTab = !showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
|
||||||
!showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened) {
|
if (opened) {
|
||||||
const originalOverflow = document.body.style.overflow;
|
const originalOverflow = document.body.style.overflow;
|
||||||
const scrollbarWidth =
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||||
window.innerWidth - document.documentElement.clientWidth;
|
|
||||||
|
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
||||||
|
|
@ -94,35 +91,27 @@ export const SettingsModal = ({
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="general"
|
value="general"
|
||||||
leftSection={
|
leftSection={<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
General
|
General
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
{showBackendsTab && (
|
{showBackendsTab && (
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="backends"
|
value="backends"
|
||||||
leftSection={
|
leftSection={<GitBranch style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<GitBranch style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Backends
|
Backends
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
)}
|
)}
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="appearance"
|
value="appearance"
|
||||||
leftSection={
|
leftSection={<Palette style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<Palette style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Appearance
|
Appearance
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="system"
|
value="system"
|
||||||
leftSection={
|
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<Monitor style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
System
|
System
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { Center, Stack, Text } from '@mantine/core';
|
||||||
import {
|
import { useEffect, useState } from 'react';
|
||||||
createSoftwareItems,
|
|
||||||
createDriverItems,
|
|
||||||
createHardwareItems,
|
|
||||||
} from '@/utils/systemInfo';
|
|
||||||
import { Stack, Text, Center } from '@mantine/core';
|
|
||||||
import { InfoCard } from '@/components/InfoCard';
|
import { InfoCard } from '@/components/InfoCard';
|
||||||
import type { HardwareInfo } from '@/types/hardware';
|
|
||||||
import type { SystemVersionInfo } from '@/types/electron';
|
import type { SystemVersionInfo } from '@/types/electron';
|
||||||
|
import type { HardwareInfo } from '@/types/hardware';
|
||||||
|
import { createDriverItems, createHardwareItems, createSoftwareItems } from '@/utils/systemInfo';
|
||||||
|
|
||||||
export const SystemTab = () => {
|
export const SystemTab = () => {
|
||||||
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(
|
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
|
||||||
null
|
|
||||||
);
|
|
||||||
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
|
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
|
||||||
const [koboldVersion, setKoboldVersion] = useState<string | null>(null);
|
const [koboldVersion, setKoboldVersion] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -30,8 +24,7 @@ export const SystemTab = () => {
|
||||||
setVersionInfo(info);
|
setVersionInfo(info);
|
||||||
setKoboldVersion(currentBackend?.version || null);
|
setKoboldVersion(currentBackend?.version || null);
|
||||||
|
|
||||||
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] =
|
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
|
||||||
await Promise.all([
|
|
||||||
window.electronAPI.kobold.detectCPU(),
|
window.electronAPI.kobold.detectCPU(),
|
||||||
window.electronAPI.kobold.detectGPU(),
|
window.electronAPI.kobold.detectGPU(),
|
||||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||||
|
|
@ -47,10 +40,7 @@ export const SystemTab = () => {
|
||||||
systemMemory,
|
systemMemory,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electronAPI.logs.logError(
|
window.electronAPI.logs.logError('Failed to load system info', error as Error);
|
||||||
'Failed to load system info',
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -77,11 +67,7 @@ export const SystemTab = () => {
|
||||||
|
|
||||||
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
|
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
|
||||||
|
|
||||||
<InfoCard
|
<InfoCard title="Hardware" items={hardwareItems} loading={!hardwareInfo} />
|
||||||
title="Hardware"
|
|
||||||
items={hardwareItems}
|
|
||||||
loading={!hardwareInfo}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { Button, Group, rem, Stack, Text, TextInput } from '@mantine/core';
|
||||||
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
|
import { ExternalLink, Folder, FolderOpen, Monitor } from 'lucide-react';
|
||||||
import { Folder, FolderOpen, Monitor, ExternalLink } from 'lucide-react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const TroubleshootingTab = () => {
|
export const TroubleshootingTab = () => {
|
||||||
const [installDir, setInstallDir] = useState('');
|
const [installDir, setInstallDir] = useState('');
|
||||||
|
|
@ -17,8 +17,7 @@ export const TroubleshootingTab = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectInstallDir = async () => {
|
const handleSelectInstallDir = async () => {
|
||||||
const selectedDir =
|
const selectedDir = await window.electronAPI.kobold.selectInstallDirectory();
|
||||||
await window.electronAPI.kobold.selectInstallDirectory();
|
|
||||||
if (selectedDir) {
|
if (selectedDir) {
|
||||||
setInstallDir(selectedDir);
|
setInstallDir(selectedDir);
|
||||||
}
|
}
|
||||||
|
|
@ -50,9 +49,7 @@ export const TroubleshootingTab = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => void handleSelectInstallDir()}
|
onClick={() => void handleSelectInstallDir()}
|
||||||
leftSection={
|
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Browse
|
Browse
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -60,9 +57,7 @@ export const TroubleshootingTab = () => {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => void handleOpenInstallDir()}
|
onClick={() => void handleOpenInstallDir()}
|
||||||
disabled={!installDir}
|
disabled={!installDir}
|
||||||
leftSection={
|
leftSection={<ExternalLink style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<ExternalLink style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -80,9 +75,7 @@ export const TroubleshootingTab = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={
|
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
onClick={() => void window.electronAPI.app.showLogsFolder()}
|
||||||
>
|
>
|
||||||
Show Logs
|
Show Logs
|
||||||
|
|
@ -90,9 +83,7 @@ export const TroubleshootingTab = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
leftSection={
|
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
||||||
<Monitor style={{ width: rem(16), height: rem(16) }} />
|
|
||||||
}
|
|
||||||
onClick={() => void window.electronAPI.app.viewConfigFile()}
|
onClick={() => void window.electronAPI.app.viewConfigFile()}
|
||||||
>
|
>
|
||||||
View Config
|
View Config
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { compareVersions } from '@/utils/version';
|
|
||||||
import { GITHUB_API } from '@/constants';
|
import { GITHUB_API } from '@/constants';
|
||||||
|
import { compareVersions } from '@/utils/version';
|
||||||
|
|
||||||
interface AppUpdateInfo {
|
interface AppUpdateInfo {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { useState, useCallback, useRef } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { logError } from '@/utils/logger';
|
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
HuggingFaceModelInfo,
|
|
||||||
HuggingFaceFileInfo,
|
HuggingFaceFileInfo,
|
||||||
|
HuggingFaceModelInfo,
|
||||||
HuggingFaceSearchParams,
|
HuggingFaceSearchParams,
|
||||||
HuggingFaceSortOption,
|
HuggingFaceSortOption,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
import { logError } from '@/utils/logger';
|
||||||
import { HUGGINGFACE_BASE_URL } from '@/constants';
|
|
||||||
|
|
||||||
const MODELS_PER_PAGE = 20;
|
const MODELS_PER_PAGE = 20;
|
||||||
const HF_API_BASE = `${HUGGINGFACE_BASE_URL}/api`;
|
const HF_API_BASE = `${HUGGINGFACE_BASE_URL}/api`;
|
||||||
|
|
@ -78,9 +77,7 @@ const fetchBaseModelParams = async (baseModelId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHuggingFaceSearch = (
|
export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) => {
|
||||||
initialParams: HuggingFaceSearchParams
|
|
||||||
) => {
|
|
||||||
const [models, setModels] = useState<HuggingFaceModelInfo[]>([]);
|
const [models, setModels] = useState<HuggingFaceModelInfo[]>([]);
|
||||||
const [files, setFiles] = useState<HuggingFaceFileInfo[]>([]);
|
const [files, setFiles] = useState<HuggingFaceFileInfo[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -88,19 +85,13 @@ export const useHuggingFaceSearch = (
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [selectedModel, setSelectedModel] = useState<HuggingFaceModelInfo>();
|
const [selectedModel, setSelectedModel] = useState<HuggingFaceModelInfo>();
|
||||||
const [sortBy, setSortBy] = useState<HuggingFaceSortOption>(
|
const [sortBy, setSortBy] = useState<HuggingFaceSortOption>(initialParams.sort);
|
||||||
initialParams.sort
|
|
||||||
);
|
|
||||||
const [searchParams] = useState<HuggingFaceSearchParams>(initialParams);
|
const [searchParams] = useState<HuggingFaceSearchParams>(initialParams);
|
||||||
const pageRef = useRef(0);
|
const pageRef = useRef(0);
|
||||||
const currentQueryRef = useRef<string | undefined>(undefined);
|
const currentQueryRef = useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
const searchModels = useCallback(
|
const searchModels = useCallback(
|
||||||
async (
|
async (query?: string, reset = true, sort: HuggingFaceSortOption = sortBy) => {
|
||||||
query?: string,
|
|
||||||
reset = true,
|
|
||||||
sort: HuggingFaceSortOption = sortBy
|
|
||||||
) => {
|
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setModels([]);
|
setModels([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
|
@ -115,21 +106,17 @@ export const useHuggingFaceSearch = (
|
||||||
currentQueryRef.current = query;
|
currentQueryRef.current = query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchQuery =
|
const searchQuery = query !== undefined ? query.trim() || undefined : searchParams.search;
|
||||||
query !== undefined ? query.trim() || undefined : searchParams.search;
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (searchQuery) params.set('search', searchQuery);
|
if (searchQuery) params.set('search', searchQuery);
|
||||||
if (searchParams.pipelineTag)
|
if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag);
|
||||||
params.set('pipeline_tag', searchParams.pipelineTag);
|
|
||||||
if (searchParams.filter) params.set('filter', searchParams.filter);
|
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');
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`${HF_API_BASE}/models?${params.toString()}`);
|
||||||
`${HF_API_BASE}/models?${params.toString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||||
|
|
@ -171,8 +158,7 @@ export const useHuggingFaceSearch = (
|
||||||
setHasMore(data.length === MODELS_PER_PAGE);
|
setHasMore(data.length === MODELS_PER_PAGE);
|
||||||
pageRef.current = 1;
|
pageRef.current = 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message = err instanceof Error ? err.message : 'Failed to search models';
|
||||||
err instanceof Error ? err.message : 'Failed to search models';
|
|
||||||
setError(message);
|
setError(message);
|
||||||
logError('HuggingFace search failed:', err as Error);
|
logError('HuggingFace search failed:', err as Error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -202,17 +188,14 @@ export const useHuggingFaceSearch = (
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (searchQuery) params.set('search', searchQuery);
|
if (searchQuery) params.set('search', searchQuery);
|
||||||
if (searchParams.pipelineTag)
|
if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag);
|
||||||
params.set('pipeline_tag', searchParams.pipelineTag);
|
|
||||||
if (searchParams.filter) params.set('filter', searchParams.filter);
|
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));
|
||||||
params.set('full', 'false');
|
params.set('full', 'false');
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`${HF_API_BASE}/models?${params.toString()}`);
|
||||||
`${HF_API_BASE}/models?${params.toString()}`
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
@ -253,8 +236,7 @@ export const useHuggingFaceSearch = (
|
||||||
setHasMore(validModels.length === MODELS_PER_PAGE);
|
setHasMore(validModels.length === MODELS_PER_PAGE);
|
||||||
pageRef.current += 1;
|
pageRef.current += 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message = err instanceof Error ? err.message : 'Failed to load more models';
|
||||||
err instanceof Error ? err.message : 'Failed to load more models';
|
|
||||||
setError(message);
|
setError(message);
|
||||||
logError('HuggingFace load more failed:', err as Error);
|
logError('HuggingFace load more failed:', err as Error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -273,9 +255,7 @@ export const useHuggingFaceSearch = (
|
||||||
const filter = searchParams?.filter;
|
const filter = searchParams?.filter;
|
||||||
const extensions = getExtensionsForLibrary(filter);
|
const extensions = getExtensionsForLibrary(filter);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`${HF_API_BASE}/models/${model.id}/tree/main?recursive=true`);
|
||||||
`${HF_API_BASE}/models/${model.id}/tree/main?recursive=true`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
throw new Error(`Failed to fetch files: ${response.statusText}`);
|
||||||
|
|
@ -285,10 +265,7 @@ export const useHuggingFaceSearch = (
|
||||||
const fileResults: HuggingFaceFileInfo[] = [];
|
const fileResults: HuggingFaceFileInfo[] = [];
|
||||||
|
|
||||||
for (const file of items) {
|
for (const file of items) {
|
||||||
if (
|
if (file.type === 'file' && extensions.some((ext) => file.path.endsWith(ext))) {
|
||||||
file.type === 'file' &&
|
|
||||||
extensions.some((ext) => file.path.endsWith(ext))
|
|
||||||
) {
|
|
||||||
fileResults.push({
|
fileResults.push({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
size: file.lfs?.size ?? file.size,
|
size: file.lfs?.size ?? file.size,
|
||||||
|
|
@ -299,8 +276,7 @@ export const useHuggingFaceSearch = (
|
||||||
fileResults.sort((a, b) => b.size - a.size);
|
fileResults.sort((a, b) => b.size - a.size);
|
||||||
setFiles(fileResults);
|
setFiles(fileResults);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message = err instanceof Error ? err.message : 'Failed to load model files';
|
||||||
err instanceof Error ? err.message : 'Failed to load model files';
|
|
||||||
setError(message);
|
setError(message);
|
||||||
logError('HuggingFace list files failed:', err as Error);
|
logError('HuggingFace list files failed:', err as Error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,7 @@ interface LaunchArgs {
|
||||||
pipelineparallel: boolean;
|
pipelineparallel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildModelArgs = (
|
const buildModelArgs = (model: string, sdmodel: string, launchArgs: LaunchArgs) => {
|
||||||
model: string,
|
|
||||||
sdmodel: string,
|
|
||||||
launchArgs: LaunchArgs
|
|
||||||
) => {
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
if (model.trim() !== '') {
|
if (model.trim() !== '') {
|
||||||
|
|
@ -99,8 +95,7 @@ const buildModelArgs = (
|
||||||
const buildConfigArgs = (isImageMode: boolean, launchArgs: LaunchArgs) => {
|
const buildConfigArgs = (isImageMode: boolean, launchArgs: LaunchArgs) => {
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
const isGpuAcceleration =
|
const isGpuAcceleration = launchArgs.acceleration && launchArgs.acceleration !== 'cpu';
|
||||||
launchArgs.acceleration && launchArgs.acceleration !== 'cpu';
|
|
||||||
|
|
||||||
if (isGpuAcceleration) {
|
if (isGpuAcceleration) {
|
||||||
if (launchArgs.autoGpuLayers && launchArgs.gpuLayers > 0) {
|
if (launchArgs.autoGpuLayers && launchArgs.gpuLayers > 0) {
|
||||||
|
|
@ -187,29 +182,23 @@ const buildClblastArgs = (launchArgs: LaunchArgs) => {
|
||||||
) {
|
) {
|
||||||
const parsed = parseCLBlastDevice(launchArgs.gpuDeviceSelection);
|
const parsed = parseCLBlastDevice(launchArgs.gpuDeviceSelection);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
clblastArgs.push(
|
clblastArgs.push(parsed.platformIndex.toString(), parsed.deviceIndex.toString());
|
||||||
parsed.platformIndex.toString(),
|
|
||||||
parsed.deviceIndex.toString()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
clblastArgs.push(launchArgs.gpuPlatform.toString(), '0');
|
clblastArgs.push(launchArgs.gpuPlatform.toString(), '0');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
clblastArgs.push(
|
clblastArgs.push(launchArgs.gpuPlatform.toString(), launchArgs.gpuDeviceSelection || '0');
|
||||||
launchArgs.gpuPlatform.toString(),
|
|
||||||
launchArgs.gpuDeviceSelection || '0'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clblastArgs;
|
return clblastArgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTensorSplitArgs = (args: string[], launchArgs: LaunchArgs) => {
|
const addTensorSplitArgs = (args: string[], launchArgs: LaunchArgs) => {
|
||||||
if (launchArgs.tensorSplit && launchArgs.tensorSplit.trim()) {
|
if (launchArgs.tensorSplit?.trim()) {
|
||||||
const tensorValues = launchArgs.tensorSplit
|
const tensorValues = launchArgs.tensorSplit
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((value) => value.trim())
|
.map((value) => value.trim())
|
||||||
.filter((value) => value !== '' && !isNaN(Number(value)));
|
.filter((value) => value !== '' && !Number.isNaN(Number(value)));
|
||||||
|
|
||||||
if (tensorValues.length > 0) {
|
if (tensorValues.length > 0) {
|
||||||
args.push('--tensorsplit', ...tensorValues);
|
args.push('--tensorsplit', ...tensorValues);
|
||||||
|
|
@ -237,10 +226,7 @@ const buildBackendArgs = (launchArgs: LaunchArgs, platform: string) => {
|
||||||
launchArgs.acceleration === 'rocm' ||
|
launchArgs.acceleration === 'rocm' ||
|
||||||
launchArgs.acceleration === 'vulkan';
|
launchArgs.acceleration === 'vulkan';
|
||||||
|
|
||||||
if (
|
if (launchArgs.acceleration === 'cuda' || launchArgs.acceleration === 'rocm') {
|
||||||
launchArgs.acceleration === 'cuda' ||
|
|
||||||
launchArgs.acceleration === 'rocm'
|
|
||||||
) {
|
|
||||||
args.push(...buildCudaArgs(launchArgs));
|
args.push(...buildCudaArgs(launchArgs));
|
||||||
|
|
||||||
if (launchArgs.gpuDeviceSelection === 'all' && isTensorSplitSupported) {
|
if (launchArgs.gpuDeviceSelection === 'all' && isTensorSplitSupported) {
|
||||||
|
|
@ -270,11 +256,7 @@ function parseCLBlastDevice(deviceString: string) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLaunchLogic = ({
|
export const useLaunchLogic = ({ model, sdmodel, onLaunch }: UseLaunchLogicProps) => {
|
||||||
model,
|
|
||||||
sdmodel,
|
|
||||||
onLaunch,
|
|
||||||
}: UseLaunchLogicProps) => {
|
|
||||||
const [isLaunching, setIsLaunching] = useState(false);
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
|
|
||||||
const handleLaunch = useCallback(
|
const handleLaunch = useCallback(
|
||||||
|
|
@ -299,29 +281,19 @@ export const useLaunchLogic = ({
|
||||||
];
|
];
|
||||||
|
|
||||||
if (launchArgs.additionalArguments.trim()) {
|
if (launchArgs.additionalArguments.trim()) {
|
||||||
const additionalArgs = launchArgs.additionalArguments
|
const additionalArgs = launchArgs.additionalArguments.trim().split(/\s+/);
|
||||||
.trim()
|
|
||||||
.split(/\s+/);
|
|
||||||
args.push(...additionalArgs);
|
args.push(...additionalArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preLaunchCommands = launchArgs.preLaunchCommands.filter(
|
const preLaunchCommands = launchArgs.preLaunchCommands.filter((cmd) => cmd.trim() !== '');
|
||||||
(cmd) => cmd.trim() !== ''
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await window.electronAPI.kobold.launchKoboldCpp(
|
const result = await window.electronAPI.kobold.launchKoboldCpp(args, preLaunchCommands);
|
||||||
args,
|
|
||||||
preLaunchCommands
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
window.electronAPI.logs.logError('Launch failed:', new Error(errorMessage));
|
||||||
'Launch failed:',
|
|
||||||
new Error(errorMessage)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLaunching(false);
|
setIsLaunching(false);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
|
import { initializeAudio, playSound, soundAssets } from '@/utils/sounds';
|
||||||
|
|
||||||
export const useLogoClickSounds = () => {
|
export const useLogoClickSounds = () => {
|
||||||
const [logoClickCount, setLogoClickCount] = useState(0);
|
const [logoClickCount, setLogoClickCount] = useState(0);
|
||||||
|
|
@ -36,9 +36,7 @@ export const useLogoClickSounds = () => {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none' as const,
|
userSelect: 'none' as const,
|
||||||
transition: 'transform 0.15s ease-in-out',
|
transition: 'transform 0.15s ease-in-out',
|
||||||
transform: isElephantMode
|
transform: isElephantMode ? 'scale(1.3) rotate(5deg)' : 'scale(1) rotate(0deg)',
|
||||||
? '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
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
|
||||||
getDisplayNameFromPath,
|
|
||||||
compareVersions,
|
|
||||||
stripAssetExtensions,
|
|
||||||
} from '@/utils/version';
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import { getROCmDownload } from '@/utils/rocm';
|
|
||||||
import type { InstalledBackend, DownloadItem } from '@/types/electron';
|
|
||||||
import type { DismissedUpdate } from '@/types';
|
import type { DismissedUpdate } from '@/types';
|
||||||
|
import type { DownloadItem, InstalledBackend } from '@/types/electron';
|
||||||
|
import { getROCmDownload } from '@/utils/rocm';
|
||||||
|
import { compareVersions, getDisplayNameFromPath, stripAssetExtensions } from '@/utils/version';
|
||||||
|
|
||||||
export interface BinaryUpdateInfo {
|
export interface BinaryUpdateInfo {
|
||||||
currentBackend: InstalledBackend;
|
currentBackend: InstalledBackend;
|
||||||
|
|
@ -18,17 +14,14 @@ export const useUpdateChecker = () => {
|
||||||
const [updateInfo, setUpdateInfo] = useState<BinaryUpdateInfo | null>(null);
|
const [updateInfo, setUpdateInfo] = useState<BinaryUpdateInfo | null>(null);
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||||
const [dismissedUpdates, setDismissedUpdates] = useState<DismissedUpdate[]>(
|
const [dismissedUpdates, setDismissedUpdates] = useState<DismissedUpdate[]>([]);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { availableDownloads: releases, loadingRemote } =
|
const { availableDownloads: releases, loadingRemote } = useKoboldBackendsStore();
|
||||||
useKoboldBackendsStore();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadDismissedUpdates = async () => {
|
const loadDismissedUpdates = async () => {
|
||||||
const dismissed = (await window.electronAPI.config.get(
|
const dismissed = (await window.electronAPI.config.get('dismissedUpdates')) as
|
||||||
'dismissedUpdates'
|
| DismissedUpdate[]
|
||||||
)) as DismissedUpdate[] | undefined;
|
| undefined;
|
||||||
|
|
||||||
if (dismissed) {
|
if (dismissed) {
|
||||||
setDismissedUpdates(dismissed);
|
setDismissedUpdates(dismissed);
|
||||||
|
|
@ -61,16 +54,13 @@ export const useUpdateChecker = () => {
|
||||||
|
|
||||||
const currentDisplayName = getDisplayNameFromPath(currentBackend);
|
const currentDisplayName = getDisplayNameFromPath(currentBackend);
|
||||||
|
|
||||||
const matchingDownload = availableDownloads.find(
|
const matchingDownload = availableDownloads.find((download: DownloadItem) => {
|
||||||
(download: DownloadItem) => {
|
|
||||||
const downloadBaseName = stripAssetExtensions(download.name);
|
const downloadBaseName = stripAssetExtensions(download.name);
|
||||||
return downloadBaseName === currentDisplayName;
|
return downloadBaseName === currentDisplayName;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingDownload && matchingDownload.version) {
|
if (matchingDownload?.version) {
|
||||||
const hasUpdate =
|
const hasUpdate = compareVersions(matchingDownload.version, currentBackend.version) > 0;
|
||||||
compareVersions(matchingDownload.version, currentBackend.version) > 0;
|
|
||||||
|
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
const isUpdateDismissed = dismissedUpdates.some(
|
const isUpdateDismissed = dismissedUpdates.some(
|
||||||
|
|
@ -93,7 +83,7 @@ export const useUpdateChecker = () => {
|
||||||
}, [dismissedUpdates, releases, loadingRemote]);
|
}, [dismissedUpdates, releases, loadingRemote]);
|
||||||
|
|
||||||
const skipUpdate = useCallback(() => {
|
const skipUpdate = useCallback(() => {
|
||||||
if (updateInfo && updateInfo.availableUpdate.version) {
|
if (updateInfo?.availableUpdate.version) {
|
||||||
const newDismissedUpdate: DismissedUpdate = {
|
const newDismissedUpdate: DismissedUpdate = {
|
||||||
currentBackendPath: updateInfo.currentBackend.path,
|
currentBackendPath: updateInfo.currentBackend.path,
|
||||||
targetVersion: updateInfo.availableUpdate.version,
|
targetVersion: updateInfo.availableUpdate.version,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { CPUCapabilities, GPUDevice } from '@/types/hardware';
|
|
||||||
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
||||||
|
import type { CPUCapabilities, GPUDevice } from '@/types/hardware';
|
||||||
|
|
||||||
export interface Warning {
|
export interface Warning {
|
||||||
type: 'warning' | 'info';
|
type: 'warning' | 'info';
|
||||||
|
|
@ -14,11 +14,7 @@ interface UseWarningsProps {
|
||||||
configLoaded?: boolean;
|
configLoaded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkModelWarnings = (
|
const checkModelWarnings = (model: string, sdmodel: string, configLoaded: boolean) => {
|
||||||
model: string,
|
|
||||||
sdmodel: string,
|
|
||||||
configLoaded: boolean
|
|
||||||
) => {
|
|
||||||
const hasTextModel = model?.trim() !== '';
|
const hasTextModel = model?.trim() !== '';
|
||||||
const hasImageModel = sdmodel.trim() !== '';
|
const hasImageModel = sdmodel.trim() !== '';
|
||||||
const showModelPriorityWarning = hasTextModel && hasImageModel;
|
const showModelPriorityWarning = hasTextModel && hasImageModel;
|
||||||
|
|
@ -37,8 +33,7 @@ const checkModelWarnings = (
|
||||||
if (showNoModelWarning) {
|
if (showNoModelWarning) {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message:
|
message: 'Select a model in the General or Image Generation tab to enable launch.',
|
||||||
'Select a model in the General or Image Generation tab to enable launch.',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,11 +59,7 @@ const checkGpuWarnings = async (
|
||||||
) => {
|
) => {
|
||||||
const warnings: Warning[] = [];
|
const warnings: Warning[] = [];
|
||||||
|
|
||||||
if (
|
if (accelerationSupport.cuda && gpuCapabilities.cuda.devices.length === 0 && gpuInfo.hasNVIDIA) {
|
||||||
accelerationSupport.cuda &&
|
|
||||||
gpuCapabilities.cuda.devices.length === 0 &&
|
|
||||||
gpuInfo.hasNVIDIA
|
|
||||||
) {
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message:
|
message:
|
||||||
|
|
@ -76,11 +67,7 @@ const checkGpuWarnings = async (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (accelerationSupport.rocm && gpuCapabilities.rocm.devices.length === 0 && gpuInfo.hasAMD) {
|
||||||
accelerationSupport.rocm &&
|
|
||||||
gpuCapabilities.rocm.devices.length === 0 &&
|
|
||||||
gpuInfo.hasAMD
|
|
||||||
) {
|
|
||||||
const platform = await window.electronAPI.kobold.getPlatform();
|
const platform = await window.electronAPI.kobold.getPlatform();
|
||||||
const baseMessage =
|
const baseMessage =
|
||||||
'Your binary supports ROCm and you have an AMD GPU, but ROCm runtime is not detected on your system.';
|
'Your binary supports ROCm and you have an AMD GPU, but ROCm runtime is not detected on your system.';
|
||||||
|
|
@ -102,9 +89,7 @@ const checkGpuWarnings = async (
|
||||||
|
|
||||||
const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
|
const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
|
||||||
const warnings: Warning[] = [];
|
const warnings: Warning[] = [];
|
||||||
const isGpuAcceleration = ['cuda', 'rocm', 'vulkan', 'clblast'].includes(
|
const isGpuAcceleration = ['cuda', 'rocm', 'vulkan', 'clblast'].includes(acceleration);
|
||||||
acceleration
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isGpuAcceleration) {
|
if (isGpuAcceleration) {
|
||||||
const gpuMemoryInfo = await window.electronAPI.kobold.detectGPUMemory();
|
const gpuMemoryInfo = await window.electronAPI.kobold.detectGPUMemory();
|
||||||
|
|
@ -115,12 +100,12 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
|
||||||
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== ''
|
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== ''
|
||||||
);
|
);
|
||||||
const lowVramGpus = validGpus.filter(
|
const lowVramGpus = validGpus.filter(
|
||||||
(gpu) => parseFloat(gpu.totalMemoryGB!) < lowVramThreshold
|
(gpu) => parseFloat(gpu.totalMemoryGB ?? '0') < lowVramThreshold
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
|
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
|
||||||
const memoryDetails = lowVramGpus
|
const memoryDetails = lowVramGpus
|
||||||
.map((gpu) => `${parseFloat(gpu.totalMemoryGB!).toFixed(1)}GB`)
|
.map((gpu) => `${parseFloat(gpu.totalMemoryGB ?? '0').toFixed(1)}GB`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
|
|
@ -134,20 +119,14 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
|
||||||
return warnings;
|
return warnings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkCpuWarnings = (
|
const checkCpuWarnings = (acceleration: string, availableAccelerations: AccelerationOption[]) => {
|
||||||
acceleration: string,
|
|
||||||
availableAccelerations: AccelerationOption[]
|
|
||||||
) => {
|
|
||||||
const warnings: Warning[] = [];
|
const warnings: Warning[] = [];
|
||||||
|
|
||||||
if (acceleration !== 'cpu') {
|
if (acceleration !== 'cpu') {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (availableAccelerations.length > 0 && availableAccelerations.some((a) => a.value === 'cpu')) {
|
||||||
availableAccelerations.length > 0 &&
|
|
||||||
availableAccelerations.some((a) => a.value === 'cpu')
|
|
||||||
) {
|
|
||||||
warnings.push({
|
warnings.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message:
|
message:
|
||||||
|
|
@ -175,11 +154,7 @@ const checkBackendWarnings = async (params?: {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gpuWarnings = await checkGpuWarnings(
|
const gpuWarnings = await checkGpuWarnings(accelerationSupport, gpuCapabilities, gpuInfo);
|
||||||
accelerationSupport,
|
|
||||||
gpuCapabilities,
|
|
||||||
gpuInfo
|
|
||||||
);
|
|
||||||
warnings.push(...gpuWarnings);
|
warnings.push(...gpuWarnings);
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
|
|
@ -189,10 +164,7 @@ const checkBackendWarnings = async (params?: {
|
||||||
warnings.push(...vramWarnings);
|
warnings.push(...vramWarnings);
|
||||||
|
|
||||||
if (cpuCapabilities) {
|
if (cpuCapabilities) {
|
||||||
const cpuWarnings = checkCpuWarnings(
|
const cpuWarnings = checkCpuWarnings(acceleration, availableAccelerations);
|
||||||
acceleration,
|
|
||||||
availableAccelerations
|
|
||||||
);
|
|
||||||
warnings.push(...cpuWarnings);
|
warnings.push(...cpuWarnings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +206,6 @@ export const useWarnings = ({
|
||||||
}, [acceleration]);
|
}, [acceleration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
void updateBackendWarnings();
|
void updateBackendWarnings();
|
||||||
}, [updateBackendWarnings]);
|
}, [updateBackendWarnings]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
|
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 { MantineProvider } from '@mantine/core';
|
|
||||||
import { App } from '@/components/App';
|
import { App } from '@/components/App';
|
||||||
import { theme, cssVariablesResolver } from '@/theme';
|
import { cssVariablesResolver, theme } from '@/theme';
|
||||||
import '@/styles/index.css';
|
import '@/styles/index.css';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
const rootElement = document.getElementById('root');
|
||||||
|
if (!rootElement) throw new Error('Root element not found');
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
/* eslint-disable no-console */
|
import { spawn } from 'node:child_process';
|
||||||
import { spawn } from 'child_process';
|
import { exit, on, platform, stderr, stdin, stdout } from 'node:process';
|
||||||
import { platform, exit, stdout, stderr, stdin, on } from 'process';
|
|
||||||
|
|
||||||
import { terminateProcess } from '@/utils/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';
|
||||||
|
|
||||||
async function getCurrentKoboldBinary() {
|
async function getCurrentKoboldBinary() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -13,9 +11,7 @@ async function getCurrentKoboldBinary() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await readJsonFile<{ currentKoboldBinary?: string }>(
|
const config = await readJsonFile<{ currentKoboldBinary?: string }>(configPath);
|
||||||
configPath
|
|
||||||
);
|
|
||||||
return config?.currentKoboldBinary || null;
|
return config?.currentKoboldBinary || null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -26,9 +22,7 @@ export async function handleCliMode(args: string[]) {
|
||||||
const currentBinary = await getCurrentKoboldBinary();
|
const currentBinary = await getCurrentKoboldBinary();
|
||||||
|
|
||||||
if (!currentBinary) {
|
if (!currentBinary) {
|
||||||
console.error(
|
console.error('Error: No binary found. Please run the GUI first to download the binary.');
|
||||||
'Error: No binary found. Please run the GUI first to download the binary.'
|
|
||||||
);
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,21 @@
|
||||||
|
import { platform } from 'node:process';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { platform } from 'process';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createMainWindow,
|
|
||||||
cleanup as cleanupWindow,
|
|
||||||
getMainWindow,
|
|
||||||
} from '@/main/modules/window';
|
|
||||||
import {
|
|
||||||
initialize as initializeConfig,
|
|
||||||
getInstallDir,
|
|
||||||
getEnableSystemTray,
|
|
||||||
getStartMinimizedToTray,
|
|
||||||
} from '@/main/modules/config';
|
|
||||||
import { createTray } from '@/main/modules/tray';
|
|
||||||
import { safeExecute } from '@/utils/node/logging';
|
|
||||||
import { stopKoboldCpp } from '@/main/modules/koboldcpp/launcher';
|
|
||||||
import { stopFrontend as stopSillyTavern } from '@/main/modules/sillytavern';
|
|
||||||
import { stopFrontend as stopOpenWebUI } from '@/main/modules/openwebui';
|
|
||||||
import { stopStaticServer } from '@/main/modules/static-server';
|
|
||||||
import { setupIPCHandlers } from '@/main/ipc';
|
|
||||||
import { ensureDir } from '@/utils/node/fs';
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
import { setupIPCHandlers } from '@/main/ipc';
|
||||||
|
import {
|
||||||
|
getEnableSystemTray,
|
||||||
|
getInstallDir,
|
||||||
|
getStartMinimizedToTray,
|
||||||
|
initialize as initializeConfig,
|
||||||
|
} from '@/main/modules/config';
|
||||||
|
import { stopKoboldCpp } from '@/main/modules/koboldcpp/launcher';
|
||||||
|
import { stopFrontend as stopOpenWebUI } from '@/main/modules/openwebui';
|
||||||
|
import { stopFrontend as stopSillyTavern } from '@/main/modules/sillytavern';
|
||||||
|
import { stopStaticServer } from '@/main/modules/static-server';
|
||||||
|
import { createTray } from '@/main/modules/tray';
|
||||||
|
import { cleanup as cleanupWindow, createMainWindow, getMainWindow } from '@/main/modules/window';
|
||||||
|
import { ensureDir } from '@/utils/node/fs';
|
||||||
|
import { safeExecute } from '@/utils/node/logging';
|
||||||
|
|
||||||
export async function initializeApp(options?: { startMinimized?: boolean }) {
|
export async function initializeApp(options?: { startMinimized?: boolean }) {
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import { argv, exit } from 'process';
|
import { argv, exit } from 'node:process';
|
||||||
|
|
||||||
if (argv[1] === '--version') {
|
if (argv[1] === '--version') {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const { app } = await import('electron');
|
const { app } = await import('electron');
|
||||||
const version = app.getVersion();
|
const version = app.getVersion();
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(version);
|
console.log(version);
|
||||||
} catch {
|
} catch {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('unknown');
|
console.log('unknown');
|
||||||
}
|
}
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|
@ -24,12 +22,10 @@ if (argv[1] === '--version') {
|
||||||
try {
|
try {
|
||||||
await cliModule.handleCliMode(args);
|
await cliModule.handleCliMode(args);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('CLI mode error:', error);
|
console.error('CLI mode error:', error);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to load CLI module:', error);
|
console.error('Failed to load CLI module:', error);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
@ -37,11 +33,8 @@ if (argv[1] === '--version') {
|
||||||
try {
|
try {
|
||||||
const guiModule = await import('./gui');
|
const guiModule = await import('./gui');
|
||||||
const startMinimized = argv.includes('--minimized');
|
const startMinimized = argv.includes('--minimized');
|
||||||
await guiModule.initializeApp(
|
await guiModule.initializeApp(startMinimized ? { startMinimized } : undefined);
|
||||||
startMinimized ? { startMinimized } : undefined
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to initialize Gerbil:', error);
|
console.error('Failed to initialize Gerbil:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
170
src/main/ipc.ts
170
src/main/ipc.ts
|
|
@ -1,63 +1,30 @@
|
||||||
import { ipcMain, app } from 'electron';
|
import { join } from 'node:path';
|
||||||
import { join } from 'path';
|
import { platform } from 'node:process';
|
||||||
import { platform } from 'process';
|
import { app, ipcMain } from 'electron';
|
||||||
import type { Screen, Acceleration } from '@/types';
|
|
||||||
import {
|
import {
|
||||||
stopKoboldCpp,
|
canAutoUpdate,
|
||||||
launchKoboldCppWithCustomFrontends,
|
checkForUpdates,
|
||||||
} from '@/main/modules/koboldcpp/launcher';
|
downloadUpdate,
|
||||||
|
isUpdateDownloaded,
|
||||||
|
quitAndInstall,
|
||||||
|
} from '@/main/modules/auto-updater';
|
||||||
import {
|
import {
|
||||||
downloadRelease,
|
|
||||||
importLocalBackend,
|
|
||||||
} from '@/main/modules/koboldcpp/download';
|
|
||||||
import {
|
|
||||||
getInstalledBackends,
|
|
||||||
getCurrentBackend,
|
|
||||||
setCurrentBackend,
|
|
||||||
deleteRelease,
|
|
||||||
} from '@/main/modules/koboldcpp/backend';
|
|
||||||
import {
|
|
||||||
getConfigFiles,
|
|
||||||
saveConfigFile,
|
|
||||||
deleteConfigFile,
|
|
||||||
parseConfigFile,
|
|
||||||
selectModelFile,
|
|
||||||
selectInstallDirectory,
|
|
||||||
} from '@/main/modules/koboldcpp/config';
|
|
||||||
import { getLocalModelsForType } from '@/main/modules/koboldcpp/model-download';
|
|
||||||
import { analyzeGGUFModel } from '@/main/modules/koboldcpp/analyze';
|
|
||||||
import {
|
|
||||||
get as getConfig,
|
|
||||||
set as setConfig,
|
|
||||||
getSelectedConfig,
|
|
||||||
getInstallDir,
|
|
||||||
getColorScheme,
|
getColorScheme,
|
||||||
|
get as getConfig,
|
||||||
getEnableSystemTray,
|
getEnableSystemTray,
|
||||||
|
getInstallDir,
|
||||||
|
getSelectedConfig,
|
||||||
|
set as setConfig,
|
||||||
} from '@/main/modules/config';
|
} from '@/main/modules/config';
|
||||||
import { createTray, updateTrayState, destroyTray } from '@/main/modules/tray';
|
|
||||||
import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
|
|
||||||
import { logError } from '@/utils/node/logging';
|
|
||||||
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
|
|
||||||
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
|
|
||||||
import {
|
import {
|
||||||
isUvAvailable,
|
|
||||||
isNpxAvailable,
|
|
||||||
getVersionInfo,
|
getVersionInfo,
|
||||||
isAURInstallation,
|
isAURInstallation,
|
||||||
|
isNpxAvailable,
|
||||||
|
isUvAvailable,
|
||||||
} from '@/main/modules/dependencies';
|
} from '@/main/modules/dependencies';
|
||||||
import { getMainWindow } from '@/main/modules/window';
|
|
||||||
import {
|
import {
|
||||||
saveTabContent,
|
|
||||||
loadTabContent,
|
|
||||||
saveNotepadState,
|
|
||||||
loadNotepadState,
|
|
||||||
deleteTabFile,
|
|
||||||
createNewTab,
|
|
||||||
renameTab,
|
|
||||||
} from '@/main/modules/notepad';
|
|
||||||
import {
|
|
||||||
detectGPU,
|
|
||||||
detectCPU,
|
detectCPU,
|
||||||
|
detectGPU,
|
||||||
detectGPUCapabilities,
|
detectGPUCapabilities,
|
||||||
detectGPUMemory,
|
detectGPUMemory,
|
||||||
detectROCm,
|
detectROCm,
|
||||||
|
|
@ -67,18 +34,44 @@ import {
|
||||||
detectAccelerationSupport,
|
detectAccelerationSupport,
|
||||||
getAvailableAccelerations,
|
getAvailableAccelerations,
|
||||||
} from '@/main/modules/koboldcpp/acceleration';
|
} from '@/main/modules/koboldcpp/acceleration';
|
||||||
|
import { analyzeGGUFModel } from '@/main/modules/koboldcpp/analyze';
|
||||||
import {
|
import {
|
||||||
openPerformanceManager,
|
deleteRelease,
|
||||||
startMonitoring,
|
getCurrentBackend,
|
||||||
stopMonitoring,
|
getInstalledBackends,
|
||||||
} from '@/main/modules/monitoring';
|
setCurrentBackend,
|
||||||
|
} from '@/main/modules/koboldcpp/backend';
|
||||||
import {
|
import {
|
||||||
checkForUpdates,
|
deleteConfigFile,
|
||||||
downloadUpdate,
|
getConfigFiles,
|
||||||
quitAndInstall,
|
parseConfigFile,
|
||||||
isUpdateDownloaded,
|
saveConfigFile,
|
||||||
canAutoUpdate,
|
selectInstallDirectory,
|
||||||
} from '@/main/modules/auto-updater';
|
selectModelFile,
|
||||||
|
} from '@/main/modules/koboldcpp/config';
|
||||||
|
import { downloadRelease, importLocalBackend } from '@/main/modules/koboldcpp/download';
|
||||||
|
import {
|
||||||
|
launchKoboldCppWithCustomFrontends,
|
||||||
|
stopKoboldCpp,
|
||||||
|
} from '@/main/modules/koboldcpp/launcher';
|
||||||
|
import { getLocalModelsForType } from '@/main/modules/koboldcpp/model-download';
|
||||||
|
import { openPerformanceManager, startMonitoring, stopMonitoring } from '@/main/modules/monitoring';
|
||||||
|
import {
|
||||||
|
createNewTab,
|
||||||
|
deleteTabFile,
|
||||||
|
loadNotepadState,
|
||||||
|
loadTabContent,
|
||||||
|
renameTab,
|
||||||
|
saveNotepadState,
|
||||||
|
saveTabContent,
|
||||||
|
} from '@/main/modules/notepad';
|
||||||
|
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||||
|
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
|
||||||
|
import { createTray, destroyTray, updateTrayState } from '@/main/modules/tray';
|
||||||
|
import { getMainWindow } from '@/main/modules/window';
|
||||||
|
import type { Acceleration, Screen } from '@/types';
|
||||||
|
import { logError } from '@/utils/node/logging';
|
||||||
|
import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
|
||||||
import { calculateOptimalGpuLayers } from '@/utils/node/vram';
|
import { calculateOptimalGpuLayers } from '@/utils/node/vram';
|
||||||
|
|
||||||
export function setupIPCHandlers() {
|
export function setupIPCHandlers() {
|
||||||
|
|
@ -98,9 +91,7 @@ export function setupIPCHandlers() {
|
||||||
saveConfigFile(configName, configData)
|
saveConfigFile(configName, configData)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:deleteConfigFile', async (_, configName) =>
|
ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => deleteConfigFile(configName));
|
||||||
deleteConfigFile(configName)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig());
|
ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig());
|
||||||
|
|
||||||
|
|
@ -108,15 +99,11 @@ export function setupIPCHandlers() {
|
||||||
setConfig('selectedConfig', configName)
|
setConfig('selectedConfig', configName)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:setCurrentBackend', (_, version) =>
|
ipcMain.handle('kobold:setCurrentBackend', (_, version) => setCurrentBackend(version));
|
||||||
setCurrentBackend(version)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir());
|
ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir());
|
||||||
|
|
||||||
ipcMain.handle('kobold:selectInstallDirectory', () =>
|
ipcMain.handle('kobold:selectInstallDirectory', () => selectInstallDirectory());
|
||||||
selectInstallDirectory()
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectGPU', () => detectGPU());
|
ipcMain.handle('kobold:detectGPU', () => detectGPU());
|
||||||
|
|
||||||
|
|
@ -130,13 +117,10 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectROCm', () => detectROCm());
|
ipcMain.handle('kobold:detectROCm', () => detectROCm());
|
||||||
|
|
||||||
ipcMain.handle('kobold:detectAccelerationSupport', () =>
|
ipcMain.handle('kobold:detectAccelerationSupport', () => detectAccelerationSupport());
|
||||||
detectAccelerationSupport()
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle('kobold:getAvailableAccelerations', (_, includeDisabled = false) =>
|
||||||
'kobold:getAvailableAccelerations',
|
getAvailableAccelerations(includeDisabled)
|
||||||
(_, includeDisabled = false) => getAvailableAccelerations(includeDisabled)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getPlatform', () => platform);
|
ipcMain.handle('kobold:getPlatform', () => platform);
|
||||||
|
|
@ -145,9 +129,7 @@ export function setupIPCHandlers() {
|
||||||
launchKoboldCppWithCustomFrontends(args, preLaunchCommands)
|
launchKoboldCppWithCustomFrontends(args, preLaunchCommands)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) =>
|
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => deleteRelease(binaryPath));
|
||||||
deleteRelease(binaryPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
||||||
void stopKoboldCpp();
|
void stopKoboldCpp();
|
||||||
|
|
@ -155,25 +137,17 @@ export function setupIPCHandlers() {
|
||||||
void stopOpenWebUIFrontend();
|
void stopOpenWebUIFrontend();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
|
ipcMain.handle('kobold:parseConfigFile', (_, filePath) => parseConfigFile(filePath));
|
||||||
parseConfigFile(filePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:selectModelFile', (_, title) =>
|
ipcMain.handle('kobold:selectModelFile', (_, title) => selectModelFile(title));
|
||||||
selectModelFile(title)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
|
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
|
||||||
|
|
||||||
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
|
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
|
||||||
getLocalModelsForType(
|
getLocalModelsForType(paramType as Parameters<typeof getLocalModelsForType>[0])
|
||||||
paramType as Parameters<typeof getLocalModelsForType>[0]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) =>
|
ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => analyzeGGUFModel(filePath));
|
||||||
analyzeGGUFModel(filePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'kobold:calculateOptimalLayers',
|
'kobold:calculateOptimalLayers',
|
||||||
|
|
@ -224,9 +198,7 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle('app:isMaximized', () => mainWindow.isMaximized());
|
ipcMain.handle('app:isMaximized', () => mainWindow.isMaximized());
|
||||||
|
|
||||||
ipcMain.handle('app:getZoomLevel', () =>
|
ipcMain.handle('app:getZoomLevel', () => mainWindow.webContents.getZoomLevel());
|
||||||
mainWindow.webContents.getZoomLevel()
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('app:setZoomLevel', (_, level) => {
|
ipcMain.handle('app:setZoomLevel', (_, level) => {
|
||||||
mainWindow.webContents.setZoomLevel(level);
|
mainWindow.webContents.setZoomLevel(level);
|
||||||
|
|
@ -250,9 +222,7 @@ export function setupIPCHandlers() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('app:getStartMinimizedToTray', () =>
|
ipcMain.handle('app:getStartMinimizedToTray', () => getConfig('startMinimizedToTray'));
|
||||||
getConfig('startMinimizedToTray')
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('app:setStartMinimizedToTray', async (_, enabled: boolean) => {
|
ipcMain.handle('app:setStartMinimizedToTray', async (_, enabled: boolean) => {
|
||||||
await setConfig('startMinimizedToTray', enabled);
|
await setConfig('startMinimizedToTray', enabled);
|
||||||
|
|
@ -291,7 +261,7 @@ export function setupIPCHandlers() {
|
||||||
ipcMain.handle('dependencies:isUvAvailable', () => isUvAvailable());
|
ipcMain.handle('dependencies:isUvAvailable', () => isUvAvailable());
|
||||||
|
|
||||||
ipcMain.handle('dependencies:clearOpenWebUIData', async () => {
|
ipcMain.handle('dependencies:clearOpenWebUIData', async () => {
|
||||||
const { rm } = await import('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, { recursive: true, force: true });
|
||||||
|
|
@ -318,15 +288,11 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle('app:isAURInstallation', () => isAURInstallation());
|
ipcMain.handle('app:isAURInstallation', () => isAURInstallation());
|
||||||
|
|
||||||
ipcMain.handle('notepad:saveTabContent', (_, title, content) =>
|
ipcMain.handle('notepad:saveTabContent', (_, title, content) => saveTabContent(title, content));
|
||||||
saveTabContent(title, content)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('notepad:loadTabContent', (_, title) => loadTabContent(title));
|
ipcMain.handle('notepad:loadTabContent', (_, title) => loadTabContent(title));
|
||||||
|
|
||||||
ipcMain.handle('notepad:renameTab', (_, oldTitle, newTitle) =>
|
ipcMain.handle('notepad:renameTab', (_, oldTitle, newTitle) => renameTab(oldTitle, newTitle));
|
||||||
renameTab(oldTitle, newTitle)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('notepad:saveState', (_, state) => saveNotepadState(state));
|
ipcMain.handle('notepad:saveState', (_, state) => saveNotepadState(state));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { platform } from 'node:process';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { platform } from 'process';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import { logError, safeExecute } from '@/utils/node/logging';
|
|
||||||
import { isDevelopment } from '@/utils/node/environment';
|
import { isDevelopment } from '@/utils/node/environment';
|
||||||
|
import { logError, safeExecute } from '@/utils/node/logging';
|
||||||
import { getAURVersion, isWindowsPortableInstallation } from './dependencies';
|
import { getAURVersion, isWindowsPortableInstallation } from './dependencies';
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
|
|
@ -42,10 +42,7 @@ export const checkForUpdates = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(await safeExecute(
|
(await safeExecute(() => autoUpdater.checkForUpdates(), 'Failed to check for updates')) !== null
|
||||||
() => autoUpdater.checkForUpdates(),
|
|
||||||
'Failed to check for updates'
|
|
||||||
)) !== null
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,10 +52,7 @@ export const downloadUpdate = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(await safeExecute(
|
(await safeExecute(() => autoUpdater.downloadUpdate(), 'Failed to download update')) !== null
|
||||||
() => autoUpdater.downloadUpdate(),
|
|
||||||
'Failed to download update'
|
|
||||||
)) !== null
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
import { homedir } from 'node:os';
|
||||||
import { safeExecute } from '@/utils/node/logging';
|
import { join } from 'node:path';
|
||||||
import { getConfigDir } from '@/utils/node/path';
|
import { platform } from 'node:process';
|
||||||
import { homedir } from 'os';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
import { join } from 'path';
|
|
||||||
import { platform } from 'process';
|
|
||||||
import { nativeTheme } from 'electron';
|
import { nativeTheme } from 'electron';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
FrontendPreference,
|
|
||||||
DismissedUpdate,
|
DismissedUpdate,
|
||||||
|
FrontendPreference,
|
||||||
ImageGenerationFrontendPreference,
|
ImageGenerationFrontendPreference,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
|
||||||
import type { SavedNotepadState } from '@/types/electron';
|
import type { SavedNotepadState } from '@/types/electron';
|
||||||
|
import { readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||||
|
import { safeExecute } from '@/utils/node/logging';
|
||||||
|
import { getConfigDir } from '@/utils/node/path';
|
||||||
|
|
||||||
export interface WindowBounds {
|
export interface WindowBounds {
|
||||||
x?: number;
|
x?: number;
|
||||||
|
|
@ -51,10 +51,7 @@ async function loadConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
const success = await safeExecute(
|
const success = await safeExecute(() => writeJsonFile(configPath, config), 'Error saving config');
|
||||||
() => writeJsonFile(configPath, config),
|
|
||||||
'Error saving config'
|
|
||||||
);
|
|
||||||
return success !== null;
|
return success !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,10 +62,7 @@ export async function initialize() {
|
||||||
|
|
||||||
export const get = <K extends keyof AppConfig>(key: K) => config[key];
|
export const get = <K extends keyof AppConfig>(key: K) => config[key];
|
||||||
|
|
||||||
export async function set<K extends keyof AppConfig>(
|
export async function set<K extends keyof AppConfig>(key: K, value: AppConfig[K]) {
|
||||||
key: K,
|
|
||||||
value: AppConfig[K]
|
|
||||||
) {
|
|
||||||
config[key] = value;
|
config[key] = value;
|
||||||
await saveConfig();
|
await saveConfig();
|
||||||
}
|
}
|
||||||
|
|
@ -123,5 +117,4 @@ export const getWindowBounds = () => config.windowBounds;
|
||||||
|
|
||||||
export const getEnableSystemTray = () => config.enableSystemTray ?? false;
|
export const getEnableSystemTray = () => config.enableSystemTray ?? false;
|
||||||
|
|
||||||
export const getStartMinimizedToTray = () =>
|
export const getStartMinimizedToTray = () => config.startMinimizedToTray ?? false;
|
||||||
config.startMinimizedToTray ?? false;
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { access, readdir, readlink } from 'fs/promises';
|
import { access, readdir, readlink } from 'node:fs/promises';
|
||||||
import { homedir, release } from 'os';
|
import { homedir, release } from 'node:os';
|
||||||
import { join } from 'path';
|
import { join } from 'node:path';
|
||||||
import { platform, env as processEnv, versions, arch } from 'process';
|
import { arch, platform, env as processEnv, versions } from 'node:process';
|
||||||
import { execa } from 'execa';
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
import { execa } from 'execa';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
|
||||||
const PATH_SEPARATOR = platform === 'win32' ? ';' : ':';
|
const PATH_SEPARATOR = platform === 'win32' ? ';' : ':';
|
||||||
|
|
@ -75,10 +75,7 @@ export async function isUvAvailable() {
|
||||||
export async function getUvEnvironment() {
|
export async function getUvEnvironment() {
|
||||||
const env = { ...processEnv };
|
const env = { ...processEnv };
|
||||||
|
|
||||||
const uvPaths = [
|
const uvPaths = [join(homedir(), '.cargo', 'bin'), join(homedir(), '.local', 'bin')];
|
||||||
join(homedir(), '.cargo', 'bin'),
|
|
||||||
join(homedir(), '.local', 'bin'),
|
|
||||||
];
|
|
||||||
|
|
||||||
const existingPaths: string[] = [];
|
const existingPaths: string[] = [];
|
||||||
for (const path of uvPaths) {
|
for (const path of uvPaths) {
|
||||||
|
|
@ -149,10 +146,7 @@ async function findNodeBinPath(baseDir: string) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryVersionManagerPath(
|
async function tryVersionManagerPath(basePath: string, env: Record<string, string | undefined>) {
|
||||||
basePath: string,
|
|
||||||
env: Record<string, string | undefined>
|
|
||||||
) {
|
|
||||||
if (!(await pathExists(basePath))) {
|
if (!(await pathExists(basePath))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -224,9 +218,11 @@ export async function isAURInstallation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVersionInfo() {
|
export async function getVersionInfo() {
|
||||||
const [nodeJsSystemVersion, uvVersion, aurPackageVersion] = await Promise.all(
|
const [nodeJsSystemVersion, uvVersion, aurPackageVersion] = await Promise.all([
|
||||||
[getSystemNodeVersion(), getUvVersion(), getAURVersion()]
|
getSystemNodeVersion(),
|
||||||
);
|
getUvVersion(),
|
||||||
|
getAURVersion(),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appVersion: app.getVersion(),
|
appVersion: app.getVersion(),
|
||||||
|
|
@ -243,10 +239,7 @@ export async function getVersionInfo() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryAddPathToEnv(
|
function tryAddPathToEnv(env: Record<string, string | undefined>, path: string) {
|
||||||
env: Record<string, string | undefined>,
|
|
||||||
path: string
|
|
||||||
) {
|
|
||||||
if (!env.PATH?.includes(path)) {
|
if (!env.PATH?.includes(path)) {
|
||||||
env.PATH = `${path}${PATH_SEPARATOR}${env.PATH}`;
|
env.PATH = `${path}${PATH_SEPARATOR}${env.PATH}`;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -269,16 +262,12 @@ export function isWindowsPortableInstallation() {
|
||||||
const execPath = app.getPath('exe');
|
const execPath = app.getPath('exe');
|
||||||
|
|
||||||
const isInTemp =
|
const isInTemp =
|
||||||
execPath.toLowerCase().includes('\\temp\\') ||
|
execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\tmp\\');
|
||||||
execPath.toLowerCase().includes('\\tmp\\');
|
|
||||||
|
|
||||||
const isInProgramFiles = execPath.toLowerCase().includes('program files');
|
const isInProgramFiles = execPath.toLowerCase().includes('program files');
|
||||||
const isInAppDataPrograms = execPath
|
const isInAppDataPrograms = execPath.toLowerCase().includes('appdata\\local\\programs');
|
||||||
.toLowerCase()
|
|
||||||
.includes('appdata\\local\\programs');
|
|
||||||
|
|
||||||
windowsPortableInstallationCache =
|
windowsPortableInstallationCache = isInTemp && !isInProgramFiles && !isInAppDataPrograms;
|
||||||
isInTemp && !isInProgramFiles && !isInAppDataPrograms;
|
|
||||||
|
|
||||||
return windowsPortableInstallationCache;
|
return windowsPortableInstallationCache;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
import {
|
import { platform } from 'node:process';
|
||||||
cpu as siCpu,
|
import { execa } from 'execa';
|
||||||
mem as siMem,
|
import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation';
|
||||||
memLayout as siMemLayout,
|
|
||||||
} from 'systeminformation';
|
|
||||||
import { safeExecute } from '@/utils/node/logging';
|
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
|
||||||
import type {
|
import type {
|
||||||
|
BasicGPUInfo,
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
GPUCapabilities,
|
GPUCapabilities,
|
||||||
BasicGPUInfo,
|
|
||||||
GPUMemoryInfo,
|
|
||||||
GPUDevice,
|
GPUDevice,
|
||||||
|
GPUMemoryInfo,
|
||||||
} from '@/types/hardware';
|
} from '@/types/hardware';
|
||||||
import { execa } from 'execa';
|
|
||||||
import { formatDeviceName } from '@/utils/format';
|
import { formatDeviceName } from '@/utils/format';
|
||||||
import { platform } from 'process';
|
import { getGPUData } from '@/utils/node/gpu';
|
||||||
import { getVulkanInfo, detectGPUViaVulkan } from '@/utils/node/vulkan';
|
import { safeExecute } from '@/utils/node/logging';
|
||||||
|
import { detectGPUViaVulkan, getVulkanInfo } from '@/utils/node/vulkan';
|
||||||
|
|
||||||
const COMMON_EXEC_OPTIONS = {
|
const COMMON_EXEC_OPTIONS = {
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
|
|
@ -65,10 +61,7 @@ export async function detectGPU() {
|
||||||
return basicGPUInfoCache;
|
return basicGPUInfoCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await safeExecute(
|
const result = await safeExecute(() => detectGPUViaVulkan(), 'GPU detection failed');
|
||||||
() => detectGPUViaVulkan(),
|
|
||||||
'GPU detection failed'
|
|
||||||
);
|
|
||||||
|
|
||||||
const fallbackGPUInfo = {
|
const fallbackGPUInfo = {
|
||||||
hasAMD: false,
|
hasAMD: false,
|
||||||
|
|
@ -172,7 +165,6 @@ async function detectCUDA() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
export async function detectROCm() {
|
export async function detectROCm() {
|
||||||
try {
|
try {
|
||||||
const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo';
|
const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo';
|
||||||
|
|
@ -188,9 +180,7 @@ export async function detectROCm() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (hipccOutput.trim()) {
|
if (hipccOutput.trim()) {
|
||||||
const hipVersionMatch = hipccOutput.match(
|
const hipVersionMatch = hipccOutput.match(/HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i);
|
||||||
/HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hipVersionMatch) {
|
if (hipVersionMatch) {
|
||||||
version = hipVersionMatch[1];
|
version = hipVersionMatch[1];
|
||||||
|
|
@ -273,11 +263,7 @@ function parseClInfoOutput(output: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findDeviceNameInClInfo(lines: string[], startIndex: number) {
|
function findDeviceNameInClInfo(lines: string[], startIndex: number) {
|
||||||
for (
|
for (let j = startIndex + 1; j < Math.min(startIndex + 50, lines.length); j++) {
|
||||||
let j = startIndex + 1;
|
|
||||||
j < Math.min(startIndex + 50, lines.length);
|
|
||||||
j++
|
|
||||||
) {
|
|
||||||
const nextLine = lines[j].trim();
|
const nextLine = lines[j].trim();
|
||||||
if (nextLine.includes('Device Board Name (AMD)')) {
|
if (nextLine.includes('Device Board Name (AMD)')) {
|
||||||
return nextLine.split('Device Board Name (AMD)')[1]?.trim() || '';
|
return nextLine.split('Device Board Name (AMD)')[1]?.trim() || '';
|
||||||
|
|
@ -287,11 +273,7 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (
|
for (let j = startIndex + 1; j < Math.min(startIndex + 100, lines.length); j++) {
|
||||||
let j = startIndex + 1;
|
|
||||||
j < Math.min(startIndex + 100, lines.length);
|
|
||||||
j++
|
|
||||||
) {
|
|
||||||
const nextLine = lines[j].trim();
|
const nextLine = lines[j].trim();
|
||||||
if (nextLine.startsWith('Device Name:')) {
|
if (nextLine.startsWith('Device Name:')) {
|
||||||
return nextLine.split('Device Name:')[1]?.trim() || '';
|
return nextLine.split('Device Name:')[1]?.trim() || '';
|
||||||
|
|
@ -305,11 +287,7 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findComputeUnitsInClInfo(lines: string[], startIndex: number) {
|
function findComputeUnitsInClInfo(lines: string[], startIndex: number) {
|
||||||
for (
|
for (let j = startIndex + 1; j < Math.min(startIndex + 50, lines.length); j++) {
|
||||||
let j = startIndex + 1;
|
|
||||||
j < Math.min(startIndex + 50, lines.length);
|
|
||||||
j++
|
|
||||||
) {
|
|
||||||
const nextLine = lines[j].trim();
|
const nextLine = lines[j].trim();
|
||||||
if (nextLine.includes('Max compute units')) {
|
if (nextLine.includes('Max compute units')) {
|
||||||
const units = nextLine
|
const units = nextLine
|
||||||
|
|
@ -347,9 +325,7 @@ function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentDevice?.name) {
|
if (currentDevice?.name) {
|
||||||
devices.push(
|
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false));
|
||||||
createDevice(currentDevice.name, currentDevice.isIntegrated || false)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices;
|
return devices;
|
||||||
|
|
@ -362,9 +338,7 @@ function handleHipInfoLine(
|
||||||
) {
|
) {
|
||||||
if (trimmedLine.startsWith('device#')) {
|
if (trimmedLine.startsWith('device#')) {
|
||||||
if (currentDevice?.name) {
|
if (currentDevice?.name) {
|
||||||
devices.push(
|
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false));
|
||||||
createDevice(currentDevice.name, currentDevice.isIntegrated || false)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -408,11 +382,7 @@ function findDeviceType(lines: string[], startIndex: number) {
|
||||||
const searchStartIndex = Math.max(0, startIndex - searchRangeLines);
|
const searchStartIndex = Math.max(0, startIndex - searchRangeLines);
|
||||||
const searchEndIndex = Math.min(lines.length, startIndex + searchRangeLines);
|
const searchEndIndex = Math.min(lines.length, startIndex + searchRangeLines);
|
||||||
|
|
||||||
for (
|
for (let searchIndex = searchStartIndex; searchIndex < searchEndIndex; searchIndex++) {
|
||||||
let searchIndex = searchStartIndex;
|
|
||||||
searchIndex < searchEndIndex;
|
|
||||||
searchIndex++
|
|
||||||
) {
|
|
||||||
if (lines[searchIndex].includes('Device Type:')) {
|
if (lines[searchIndex].includes('Device Type:')) {
|
||||||
return lines[searchIndex].split('Device Type:')[1]?.trim() || '';
|
return lines[searchIndex].split('Device Type:')[1]?.trim() || '';
|
||||||
}
|
}
|
||||||
|
|
@ -420,10 +390,7 @@ function findDeviceType(lines: string[], startIndex: number) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function determineIfIntegrated(
|
function determineIfIntegrated(name: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
||||||
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)
|
||||||
|
|
@ -492,8 +459,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.clockSpeed && slot.clockSpeed > 0) || (slot.type && slot.type !== ''))
|
||||||
(slot.type && slot.type !== ''))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (populatedSlot) {
|
if (populatedSlot) {
|
||||||
|
|
@ -501,10 +467,7 @@ export const detectSystemMemory = async () => {
|
||||||
populatedSlot.clockSpeed && populatedSlot.clockSpeed > 0
|
populatedSlot.clockSpeed && populatedSlot.clockSpeed > 0
|
||||||
? populatedSlot.clockSpeed
|
? populatedSlot.clockSpeed
|
||||||
: undefined;
|
: undefined;
|
||||||
type =
|
type = populatedSlot.type && populatedSlot.type !== '' ? populatedSlot.type : undefined;
|
||||||
populatedSlot.type && populatedSlot.type !== ''
|
|
||||||
? populatedSlot.type
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { join, dirname } from 'path';
|
import { dirname, join } from 'node:path';
|
||||||
import { platform } from 'process';
|
import { platform } from 'node:process';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
|
||||||
import { getCurrentBinaryInfo } from './backend';
|
|
||||||
import { detectGPUCapabilities, detectCPU } from '../hardware';
|
|
||||||
import { tryExecute, safeExecute } from '@/utils/node/logging';
|
|
||||||
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
import type { AccelerationOption, AccelerationSupport } from '@/types';
|
||||||
|
import { pathExists } from '@/utils/node/fs';
|
||||||
|
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
|
import { detectCPU, detectGPUCapabilities } from '../hardware';
|
||||||
|
import { getCurrentBinaryInfo } from './backend';
|
||||||
|
|
||||||
const accelerationSupportCache = new Map<string, AccelerationSupport>();
|
const accelerationSupportCache = new Map<string, AccelerationSupport>();
|
||||||
const availableAccelerationsCache = new Map<string, AccelerationOption[]>();
|
const availableAccelerationsCache = new Map<string, AccelerationOption[]>();
|
||||||
|
|
@ -12,8 +12,9 @@ const availableAccelerationsCache = new Map<string, AccelerationOption[]>();
|
||||||
const CPU_LABEL = platform === 'darwin' ? 'Metal' : 'CPU';
|
const CPU_LABEL = platform === 'darwin' ? 'Metal' : 'CPU';
|
||||||
|
|
||||||
async function detectAccelerationSupportFromPath(koboldBinaryPath: string) {
|
async function detectAccelerationSupportFromPath(koboldBinaryPath: string) {
|
||||||
if (accelerationSupportCache.has(koboldBinaryPath)) {
|
const cached = accelerationSupportCache.get(koboldBinaryPath);
|
||||||
return accelerationSupportCache.get(koboldBinaryPath)!;
|
if (cached) {
|
||||||
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
const support: AccelerationSupport = {
|
const support: AccelerationSupport = {
|
||||||
|
|
@ -84,10 +85,8 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
return [{ value: 'cpu', label: CPU_LABEL }];
|
return [{ value: 'cpu', label: CPU_LABEL }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const result = await safeExecute(async () => {
|
const result = await safeExecute(async () => {
|
||||||
const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] =
|
const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] = await Promise.all([
|
||||||
await Promise.all([
|
|
||||||
getCurrentBinaryInfo(),
|
getCurrentBinaryInfo(),
|
||||||
detectGPUCapabilities(),
|
detectGPUCapabilities(),
|
||||||
includeDisabled ? detectCPU() : Promise.resolve(null),
|
includeDisabled ? detectCPU() : Promise.resolve(null),
|
||||||
|
|
@ -99,8 +98,9 @@ export async function getAvailableAccelerations(includeDisabled = false) {
|
||||||
|
|
||||||
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
|
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
|
||||||
|
|
||||||
if (availableAccelerationsCache.has(cacheKey)) {
|
const cached = availableAccelerationsCache.get(cacheKey);
|
||||||
return availableAccelerationsCache.get(cacheKey)!;
|
if (cached) {
|
||||||
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accelerationSupport = await detectAccelerationSupport();
|
const accelerationSupport = await detectAccelerationSupport();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { stat } from 'node:fs/promises';
|
||||||
import { gguf } from '@huggingface/gguf';
|
import { gguf } from '@huggingface/gguf';
|
||||||
import { stat } from 'fs/promises';
|
|
||||||
import { logError } from '@/utils/node/logging';
|
|
||||||
import { formatBytes } from '@/utils/format';
|
import { formatBytes } from '@/utils/format';
|
||||||
|
import { logError } from '@/utils/node/logging';
|
||||||
|
|
||||||
function estimateMemoryRequirements(fileSize: number) {
|
function estimateMemoryRequirements(fileSize: number) {
|
||||||
const vramOverhead = 1.1;
|
const vramOverhead = 1.1;
|
||||||
|
|
@ -43,8 +43,7 @@ function getMetadataValue(metadata: Record<string, unknown>, key: string) {
|
||||||
|
|
||||||
export async function analyzeGGUFModel(filePath: string) {
|
export async function analyzeGGUFModel(filePath: string) {
|
||||||
try {
|
try {
|
||||||
const isUrl =
|
const isUrl = filePath.startsWith('http://') || filePath.startsWith('https://');
|
||||||
filePath.startsWith('http://') || filePath.startsWith('https://');
|
|
||||||
|
|
||||||
let fileSize: number;
|
let fileSize: number;
|
||||||
if (isUrl) {
|
if (isUrl) {
|
||||||
|
|
@ -62,31 +61,22 @@ export async function analyzeGGUFModel(filePath: string) {
|
||||||
|
|
||||||
const metadataRecord = metadata as Record<string, unknown>;
|
const metadataRecord = metadata as Record<string, unknown>;
|
||||||
|
|
||||||
const architecture = getMetadataValue(
|
const architecture = getMetadataValue(metadataRecord, 'general.architecture') as string;
|
||||||
metadataRecord,
|
const name = getMetadataValue(metadataRecord, 'general.name') as string | undefined;
|
||||||
'general.architecture'
|
const paramCount = getMetadataValue(metadataRecord, 'general.parameter_count') as
|
||||||
) as string;
|
| number
|
||||||
const name = getMetadataValue(metadataRecord, 'general.name') as
|
|
||||||
| string
|
|
||||||
| undefined;
|
| undefined;
|
||||||
const paramCount = getMetadataValue(
|
|
||||||
metadataRecord,
|
|
||||||
'general.parameter_count'
|
|
||||||
) as number | undefined;
|
|
||||||
|
|
||||||
const contextLength = getMetadataValue(
|
const contextLength = getMetadataValue(metadataRecord, `${architecture}.context_length`) as
|
||||||
metadataRecord,
|
| number
|
||||||
`${architecture}.context_length`
|
| undefined;
|
||||||
) as number | undefined;
|
|
||||||
|
|
||||||
const blockCount = getMetadataValue(
|
const blockCount = getMetadataValue(metadataRecord, `${architecture}.block_count`) as
|
||||||
metadataRecord,
|
| number
|
||||||
`${architecture}.block_count`
|
| undefined;
|
||||||
) as number | undefined;
|
const expertCount = getMetadataValue(metadataRecord, `${architecture}.expert_count`) as
|
||||||
const expertCount = getMetadataValue(
|
| number
|
||||||
metadataRecord,
|
| undefined;
|
||||||
`${architecture}.expert_count`
|
|
||||||
) as number | undefined;
|
|
||||||
|
|
||||||
const memoryEstimates = estimateMemoryRequirements(fileSize);
|
const memoryEstimates = estimateMemoryRequirements(fileSize);
|
||||||
const vramPerLayer = estimateVramPerLayer(fileSize, blockCount);
|
const vramPerLayer = estimateVramPerLayer(fileSize, blockCount);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,14 @@
|
||||||
import { readdir, stat, rm } from 'fs/promises';
|
import { readdir, rm, stat } from 'node:fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'node:path';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
|
import type { InstalledBackend } from '@/types/electron';
|
||||||
import {
|
|
||||||
getCurrentKoboldBinary,
|
|
||||||
setCurrentKoboldBinary,
|
|
||||||
getInstallDir,
|
|
||||||
} from '../config';
|
|
||||||
import { sendToRenderer } from '../window';
|
|
||||||
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 type { InstalledBackend } from '@/types/electron';
|
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
|
||||||
|
import { sendToRenderer } from '../window';
|
||||||
|
|
||||||
const backendVersionCache = new Map<
|
const backendVersionCache = new Map<string, { version: string; actualVersion?: string } | null>();
|
||||||
string,
|
|
||||||
{ version: string; actualVersion?: string } | null
|
|
||||||
>();
|
|
||||||
|
|
||||||
export function clearBackendVersionCache(path?: string) {
|
export function clearBackendVersionCache(path?: string) {
|
||||||
if (path) {
|
if (path) {
|
||||||
|
|
@ -70,18 +62,13 @@ export async function getInstalledBackends() {
|
||||||
actualVersion: versionInfo.actualVersion,
|
actualVersion: versionInfo.actualVersion,
|
||||||
} as InstalledBackend;
|
} as InstalledBackend;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(
|
logError(`Could not detect version for ${launcher.filename}:`, error as Error);
|
||||||
`Could not detect version for ${launcher.filename}:`,
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = await Promise.all(versionPromises);
|
const results = await Promise.all(versionPromises);
|
||||||
return results.filter(
|
return results.filter((version): version is InstalledBackend => version !== null);
|
||||||
(version): version is InstalledBackend => version !== null
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError('Error scanning install directory:', error as Error);
|
logError('Error scanning install directory:', error as Error);
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -93,9 +80,7 @@ export async function getCurrentBackend() {
|
||||||
const backends = await getInstalledBackends();
|
const backends = await getInstalledBackends();
|
||||||
|
|
||||||
if (currentBinaryPath && (await pathExists(currentBinaryPath))) {
|
if (currentBinaryPath && (await pathExists(currentBinaryPath))) {
|
||||||
const currentBackend = backends.find(
|
const currentBackend = backends.find((b: InstalledBackend) => b.path === currentBinaryPath);
|
||||||
(b: InstalledBackend) => b.path === currentBinaryPath
|
|
||||||
);
|
|
||||||
if (currentBackend) {
|
if (currentBackend) {
|
||||||
return currentBackend;
|
return currentBackend;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { join } from 'path';
|
import { readdir, stat, unlink } from 'node:fs/promises';
|
||||||
import { readdir, stat, unlink } from 'fs/promises';
|
import { join } from 'node:path';
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
|
||||||
import { getInstallDir, setInstallDir } from '../config';
|
|
||||||
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
|
||||||
import { getMainWindow, sendToRenderer } from '../window';
|
|
||||||
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 { logError, safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
|
import { getInstallDir, setInstallDir } from '../config';
|
||||||
|
import { getMainWindow, sendToRenderer } from '../window';
|
||||||
|
|
||||||
export async function getConfigFiles() {
|
export async function getConfigFiles() {
|
||||||
const configFiles: { name: string; path: string; size: number }[] = [];
|
const configFiles: { name: string; path: string; size: number }[] = [];
|
||||||
|
|
@ -23,9 +22,7 @@ export async function getConfigFiles() {
|
||||||
const stats = await stat(filePath);
|
const stats = await stat(filePath);
|
||||||
if (
|
if (
|
||||||
stats.isFile() &&
|
stats.isFile() &&
|
||||||
(file.endsWith('.kcpps') ||
|
(file.endsWith('.kcpps') || file.endsWith('.kcppt') || file.endsWith('.json'))
|
||||||
file.endsWith('.kcppt') ||
|
|
||||||
file.endsWith('.json'))
|
|
||||||
) {
|
) {
|
||||||
configFiles.push({
|
configFiles.push({
|
||||||
name: file,
|
name: file,
|
||||||
|
|
@ -53,10 +50,7 @@ export async function parseConfigFile(filePath: string) {
|
||||||
}, 'Error parsing config file');
|
}, 'Error parsing config file');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveConfigFile(
|
export async function saveConfigFile(configFileName: string, configData: KoboldConfig) {
|
||||||
configFileName: string,
|
|
||||||
configData: KoboldConfig
|
|
||||||
) {
|
|
||||||
return tryExecute(async () => {
|
return tryExecute(async () => {
|
||||||
const installDir = getInstallDir();
|
const installDir = getInstallDir();
|
||||||
const configPath = join(installDir, configFileName);
|
const configPath = join(installDir, configFileName);
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,28 @@
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'node:fs';
|
||||||
import { join, basename } from 'path';
|
import { chmod, copyFile, mkdir, rename, rm, unlink } from 'node:fs/promises';
|
||||||
import { platform } from 'process';
|
import { basename, join } from 'node:path';
|
||||||
import { rm, unlink, rename, mkdir, chmod, copyFile } from 'fs/promises';
|
import { platform } from 'node:process';
|
||||||
import { execa } from 'execa';
|
|
||||||
import { dialog } from 'electron';
|
import { dialog } from 'electron';
|
||||||
|
import { execa } from 'execa';
|
||||||
import {
|
|
||||||
getInstallDir,
|
|
||||||
getCurrentKoboldBinary,
|
|
||||||
setCurrentKoboldBinary,
|
|
||||||
} from '../config';
|
|
||||||
import { logError } from '@/utils/node/logging';
|
|
||||||
import { getMainWindow, sendToRenderer } from '../window';
|
|
||||||
import { pathExists } from '@/utils/node/fs';
|
|
||||||
import { stripAssetExtensions } from '@/utils/version';
|
|
||||||
import { getLauncherPath } from '@/utils/node/path';
|
|
||||||
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
|
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
|
||||||
|
import { pathExists } from '@/utils/node/fs';
|
||||||
|
import { logError } from '@/utils/node/logging';
|
||||||
|
import { getLauncherPath } from '@/utils/node/path';
|
||||||
|
import { stripAssetExtensions } from '@/utils/version';
|
||||||
|
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
|
||||||
|
import { getMainWindow, sendToRenderer } from '../window';
|
||||||
import { clearBackendVersionCache, getVersionFromBinary } from './backend';
|
import { clearBackendVersionCache, getVersionFromBinary } from './backend';
|
||||||
|
|
||||||
async function removeDirectoryWithRetry(
|
async function removeDirectoryWithRetry(dirPath: string, maxRetries = 3, currentRetry = 0) {
|
||||||
dirPath: string,
|
|
||||||
maxRetries = 3,
|
|
||||||
currentRetry = 0
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
await rm(dirPath, { recursive: true, force: true });
|
await rm(dirPath, { recursive: true, force: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentRetry < maxRetries) {
|
if (currentRetry < maxRetries) {
|
||||||
const delay = Math.pow(2, currentRetry) * 1000;
|
const delay = 2 ** currentRetry * 1000;
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
return removeDirectoryWithRetry(dirPath, maxRetries, currentRetry + 1);
|
return removeDirectoryWithRetry(dirPath, maxRetries, currentRetry + 1);
|
||||||
} else {
|
} else {
|
||||||
logError(
|
logError(`Failed to remove directory after ${maxRetries} retries:`, error as Error);
|
||||||
`Failed to remove directory after ${maxRetries} retries:`,
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,10 +39,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(
|
const totalBytes = parseInt(response.headers.get('content-length') || '0', 10);
|
||||||
response.headers.get('content-length') || '0',
|
|
||||||
10
|
|
||||||
);
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
|
|
||||||
const pump = async () => {
|
const pump = async () => {
|
||||||
|
|
@ -102,10 +87,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupLauncher(
|
async function setupLauncher(tempPackedFilePath: string, unpackedDirPath: string) {
|
||||||
tempPackedFilePath: string,
|
|
||||||
unpackedDirPath: string
|
|
||||||
) {
|
|
||||||
let launcherPath = await getLauncherPath(unpackedDirPath);
|
let launcherPath = await getLauncherPath(unpackedDirPath);
|
||||||
|
|
||||||
if (!launcherPath || !(await pathExists(launcherPath))) {
|
if (!launcherPath || !(await pathExists(launcherPath))) {
|
||||||
|
|
@ -149,8 +131,7 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
|
||||||
stdout?: string;
|
stdout?: string;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
const errorMessage =
|
const errorMessage = execaError.stderr || execaError.stdout || execaError.message;
|
||||||
execaError.stderr || execaError.stdout || execaError.message;
|
|
||||||
throw new Error(`Unpack failed: ${errorMessage}`);
|
throw new Error(`Unpack failed: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,15 +189,10 @@ async function installBackend({
|
||||||
return launcherPath;
|
return launcherPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadRelease(
|
export async function downloadRelease(asset: GitHubAsset, options: DownloadReleaseOptions) {
|
||||||
asset: GitHubAsset,
|
|
||||||
options: DownloadReleaseOptions
|
|
||||||
) {
|
|
||||||
const tempPackedFilePath = join(getInstallDir(), `${asset.name}.packed`);
|
const tempPackedFilePath = join(getInstallDir(), `${asset.name}.packed`);
|
||||||
const baseFilename = stripAssetExtensions(asset.name);
|
const baseFilename = stripAssetExtensions(asset.name);
|
||||||
const folderName = asset.version
|
const folderName = asset.version ? `${baseFilename}-${asset.version}` : baseFilename;
|
||||||
? `${baseFilename}-${asset.version}`
|
|
||||||
: baseFilename;
|
|
||||||
const unpackedDirPath = join(getInstallDir(), folderName);
|
const unpackedDirPath = join(getInstallDir(), folderName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -264,8 +240,7 @@ export async function importLocalBackend() {
|
||||||
if (!backendVersion || backendVersion.version === 'unknown') {
|
if (!backendVersion || backendVersion.version === 'unknown') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: 'Invalid backend executable. Could not determine version information.',
|
||||||
'Invalid backend executable. Could not determine version information.',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,25 @@
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { type ChildProcess, spawn } from 'node:child_process';
|
||||||
import { platform } from 'process';
|
import { platform } from 'node:process';
|
||||||
|
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
|
||||||
import { logError, safeExecute } from '@/utils/node/logging';
|
|
||||||
import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
|
|
||||||
import { SERVER_READY_SIGNALS } from '@/constants';
|
import { SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import type { KoboldCrashInfo } from '@/types/ipc';
|
import { get as getConfig, getCurrentKoboldBinary, getInstallDir } from '@/main/modules/config';
|
||||||
import { pathExists } from '@/utils/node/fs';
|
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
|
||||||
import { getCurrentBackend } from '../backend';
|
|
||||||
import {
|
|
||||||
getCurrentKoboldBinary,
|
|
||||||
get as getConfig,
|
|
||||||
getInstallDir,
|
|
||||||
} from '@/main/modules/config';
|
|
||||||
import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern';
|
|
||||||
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
|
||||||
import {
|
import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern';
|
||||||
patchKliteEmbd,
|
import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
|
||||||
patchKcppSduiEmbd,
|
|
||||||
patchLcppGzEmbd,
|
|
||||||
filterSpam,
|
|
||||||
} from './patches';
|
|
||||||
import { startProxy, stopProxy } from '../proxy';
|
|
||||||
import { startTunnel, stopTunnel } from '../tunnel';
|
|
||||||
import { resolveModelPath, abortActiveDownloads } from '../model-download';
|
|
||||||
import type {
|
import type {
|
||||||
FrontendPreference,
|
FrontendPreference,
|
||||||
ImageGenerationFrontendPreference,
|
ImageGenerationFrontendPreference,
|
||||||
ModelParamType,
|
ModelParamType,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
import type { KoboldCrashInfo } from '@/types/ipc';
|
||||||
|
import { pathExists } from '@/utils/node/fs';
|
||||||
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
|
import { logError, safeExecute } from '@/utils/node/logging';
|
||||||
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
|
import { getCurrentBackend } from '../backend';
|
||||||
|
import { abortActiveDownloads, resolveModelPath } from '../model-download';
|
||||||
|
import { startProxy, stopProxy } from '../proxy';
|
||||||
|
import { startTunnel, stopTunnel } from '../tunnel';
|
||||||
|
import { filterSpam, patchKcppSduiEmbd, patchKliteEmbd, patchLcppGzEmbd } from './patches';
|
||||||
|
|
||||||
let koboldProcess: ChildProcess | null = null;
|
let koboldProcess: ChildProcess | null = null;
|
||||||
let isIntentionalStop = false;
|
let isIntentionalStop = false;
|
||||||
|
|
@ -73,9 +63,7 @@ function spawnPreLaunchCommands(commands: string[]) {
|
||||||
if (code !== 0 && code !== null) {
|
if (code !== 0 && code !== null) {
|
||||||
sendKoboldOutput(`Command "${command}" exited with code ${code}\n`);
|
sendKoboldOutput(`Command "${command}" exited with code ${code}\n`);
|
||||||
} else if (signal) {
|
} else if (signal) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(`Command "${command}" terminated with signal ${signal}\n`);
|
||||||
`Command "${command}" terminated with signal ${signal}\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -87,9 +75,7 @@ function spawnPreLaunchCommands(commands: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopPreLaunchProcesses() {
|
async function stopPreLaunchProcesses() {
|
||||||
const terminations = Array.from(preLaunchProcesses).map((process) =>
|
const terminations = Array.from(preLaunchProcesses).map((process) => terminateProcess(process));
|
||||||
terminateProcess(process)
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(terminations);
|
await Promise.all(terminations);
|
||||||
preLaunchProcesses.clear();
|
preLaunchProcesses.clear();
|
||||||
|
|
@ -126,8 +112,7 @@ async function resolveModelPaths(args: string[]) {
|
||||||
resolvedArgs.push(resolvedPath);
|
resolvedArgs.push(resolvedPath);
|
||||||
i++;
|
i++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
if (errorMessage.includes('aborted')) {
|
if (errorMessage.includes('aborted')) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -225,9 +210,8 @@ export async function launchKoboldCpp(
|
||||||
void startTunnel(frontendPreference);
|
void startTunnel(frontendPreference);
|
||||||
}
|
}
|
||||||
|
|
||||||
let readyResolve:
|
let readyResolve: ((value: { success: boolean; pid?: number; error?: string }) => void) | null =
|
||||||
| ((value: { success: boolean; pid?: number; error?: string }) => void)
|
null;
|
||||||
| null = null;
|
|
||||||
let readyReject: ((error: Error) => void) | null = null;
|
let readyReject: ((error: Error) => void) | null = null;
|
||||||
let isReady = false;
|
let isReady = false;
|
||||||
|
|
||||||
|
|
@ -275,7 +259,7 @@ export async function launchKoboldCpp(
|
||||||
const displayMessage = signal
|
const displayMessage = signal
|
||||||
? `\nProcess terminated with signal ${signal}`
|
? `\nProcess terminated with signal ${signal}`
|
||||||
: code === 0
|
: code === 0
|
||||||
? `\nProcess exited successfully`
|
? '\nProcess exited successfully'
|
||||||
: code && (code > 1 || code < 0)
|
: code && (code > 1 || code < 0)
|
||||||
? `\nProcess exited with code ${code}`
|
? `\nProcess exited with code ${code}`
|
||||||
: `\nProcess exited with code ${code}`;
|
: `\nProcess exited with code ${code}`;
|
||||||
|
|
@ -297,9 +281,7 @@ export async function launchKoboldCpp(
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
readyReject?.(
|
readyReject?.(
|
||||||
new Error(
|
new Error(`Process exited before ready signal (code: ${code}, signal: ${signal})`)
|
||||||
`Process exited before ready signal (code: ${code}, signal: ${signal})`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -347,9 +329,7 @@ export const launchKoboldCppWithCustomFrontends = async (
|
||||||
) =>
|
) =>
|
||||||
safeExecute(async () => {
|
safeExecute(async () => {
|
||||||
const frontendPreference = getConfig('frontendPreference');
|
const frontendPreference = getConfig('frontendPreference');
|
||||||
const imageGenerationFrontendPreference = getConfig(
|
const imageGenerationFrontendPreference = getConfig('imageGenerationFrontendPreference');
|
||||||
'imageGenerationFrontendPreference'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isTextMode } = parseKoboldConfig(args);
|
const { isTextMode } = parseKoboldConfig(args);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { readFile, writeFile, copyFile } from 'fs/promises';
|
import { copyFile, readFile, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { tryExecute } from '@/utils/node/logging';
|
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
|
import { tryExecute } from '@/utils/node/logging';
|
||||||
import { getAssetPath } from '@/utils/node/path';
|
import { getAssetPath } from '@/utils/node/path';
|
||||||
|
|
||||||
const KLITE_CSS_OVERRIDE = `
|
const KLITE_CSS_OVERRIDE = `
|
||||||
|
|
@ -86,10 +85,7 @@ export const patchKliteEmbd = (unpackedDir: string) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
patchedContent = patchedContent.replace(
|
patchedContent = patchedContent.replace('</head>', `${KLITE_CSS_OVERRIDE}\n</head>`);
|
||||||
'</head>',
|
|
||||||
`${KLITE_CSS_OVERRIDE}\n</head>`
|
|
||||||
);
|
|
||||||
|
|
||||||
await writeFile(kliteEmbdPath, patchedContent, 'utf8');
|
await writeFile(kliteEmbdPath, patchedContent, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { join, basename, dirname } from 'path';
|
import { createWriteStream } from 'node:fs';
|
||||||
import { mkdir, readdir, stat, rename, unlink } from 'fs/promises';
|
import { mkdir, readdir, rename, stat, unlink } from 'node:fs/promises';
|
||||||
import { createWriteStream } from 'fs';
|
import type { IncomingMessage } from 'node:http';
|
||||||
import { get as httpGet } from 'http';
|
import { get as httpGet } from 'node:http';
|
||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'node:https';
|
||||||
|
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 type { CachedModel, ModelParamType } from '@/types';
|
||||||
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 { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
|
|
||||||
import type { ModelParamType, CachedModel } from '@/types';
|
|
||||||
import type { IncomingMessage } from 'http';
|
|
||||||
|
|
||||||
const activeDownloads = new Set<{
|
const activeDownloads = new Set<{
|
||||||
abort: () => void;
|
abort: () => void;
|
||||||
|
|
@ -27,9 +27,7 @@ interface DownloadProgress {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHuggingFaceUrl(url: string) {
|
function parseHuggingFaceUrl(url: string) {
|
||||||
const hfMatch = url.match(
|
const hfMatch = url.match(/huggingface\.co\/([^/]+)\/([^/]+)\/(?:resolve|blob)\/[^/]+\/(.+)/);
|
||||||
/huggingface\.co\/([^/]+)\/([^/]+)\/(?:resolve|blob)\/[^/]+\/(.+)/
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hfMatch) {
|
if (hfMatch) {
|
||||||
const pathWithQuery = hfMatch[3];
|
const pathWithQuery = hfMatch[3];
|
||||||
|
|
@ -131,13 +129,11 @@ async function downloadFile(
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
activeDownloads.delete(abortController);
|
activeDownloads.delete(abortController);
|
||||||
reject(
|
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
||||||
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBytes = parseInt(response.headers['content-length'] || '0');
|
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;
|
||||||
|
|
@ -154,23 +150,17 @@ async function downloadFile(
|
||||||
if (timeDiff >= 0.5) {
|
if (timeDiff >= 0.5) {
|
||||||
const bytesDiff = downloadedBytes - lastReportedBytes;
|
const bytesDiff = downloadedBytes - lastReportedBytes;
|
||||||
const speedBytesPerSec = bytesDiff / timeDiff;
|
const speedBytesPerSec = bytesDiff / timeDiff;
|
||||||
const percent = totalBytes
|
const percent = totalBytes ? Math.round((downloadedBytes / totalBytes) * 100) : 0;
|
||||||
? Math.round((downloadedBytes / totalBytes) * 100)
|
|
||||||
: 0;
|
|
||||||
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(2);
|
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(2);
|
||||||
const totalMB = (totalBytes / 1024 / 1024).toFixed(2);
|
const totalMB = (totalBytes / 1024 / 1024).toFixed(2);
|
||||||
const speedMBPerSec = (speedBytesPerSec / 1024 / 1024).toFixed(2);
|
const speedMBPerSec = (speedBytesPerSec / 1024 / 1024).toFixed(2);
|
||||||
|
|
||||||
const remainingBytes = totalBytes - downloadedBytes;
|
const remainingBytes = totalBytes - downloadedBytes;
|
||||||
const etaSeconds = speedBytesPerSec
|
const etaSeconds = speedBytesPerSec ? Math.round(remainingBytes / speedBytesPerSec) : 0;
|
||||||
? Math.round(remainingBytes / speedBytesPerSec)
|
|
||||||
: 0;
|
|
||||||
const etaMinutes = Math.floor(etaSeconds / 60);
|
const etaMinutes = Math.floor(etaSeconds / 60);
|
||||||
const etaSecondsRemainder = etaSeconds % 60;
|
const etaSecondsRemainder = etaSeconds % 60;
|
||||||
const etaStr =
|
const etaStr =
|
||||||
etaMinutes > 0
|
etaMinutes > 0 ? `${etaMinutes}m${etaSecondsRemainder}s` : `${etaSeconds}s`;
|
||||||
? `${etaMinutes}m${etaSecondsRemainder}s`
|
|
||||||
: `${etaSeconds}s`;
|
|
||||||
|
|
||||||
const progressMsg = totalBytes
|
const progressMsg = totalBytes
|
||||||
? `Downloaded ${downloadedMB}MB / ${totalMB}MB (${percent}%) - ${speedMBPerSec}MB/s - ETA: ${etaStr}`
|
? `Downloaded ${downloadedMB}MB / ${totalMB}MB (${percent}%) - ${speedMBPerSec}MB/s - ETA: ${etaStr}`
|
||||||
|
|
@ -366,6 +356,8 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
|
||||||
|
|
||||||
export function abortActiveDownloads() {
|
export function abortActiveDownloads() {
|
||||||
const downloads = Array.from(activeDownloads);
|
const downloads = Array.from(activeDownloads);
|
||||||
downloads.forEach((controller) => controller.abort());
|
for (const controller of downloads) {
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
activeDownloads.clear();
|
activeDownloads.clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import http from 'http';
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
import http from 'node:http';
|
||||||
|
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';
|
||||||
import { PROXY } from '@/constants/proxy';
|
|
||||||
|
|
||||||
let proxyServer: http.Server | null = null;
|
let proxyServer: http.Server | null = null;
|
||||||
let koboldCppHost = 'localhost';
|
let koboldCppHost = 'localhost';
|
||||||
|
|
@ -17,10 +16,7 @@ const replaceKoboldWithGerbil = (data: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxyRequest = (
|
const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) => {
|
||||||
clientReq: IncomingMessage,
|
|
||||||
clientRes: ServerResponse
|
|
||||||
) => {
|
|
||||||
const options = {
|
const options = {
|
||||||
hostname: koboldCppHost,
|
hostname: koboldCppHost,
|
||||||
port: koboldCppPort,
|
port: koboldCppPort,
|
||||||
|
|
@ -30,8 +26,7 @@ const proxyRequest = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const proxyReq = http.request(options, (proxyRes) => {
|
const proxyReq = http.request(options, (proxyRes) => {
|
||||||
const isJson =
|
const isJson = proxyRes.headers['content-type']?.includes('application/json');
|
||||||
proxyRes.headers['content-type']?.includes('application/json');
|
|
||||||
|
|
||||||
if (isJson) {
|
if (isJson) {
|
||||||
let body = '';
|
let body = '';
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'node:fs';
|
||||||
import { chmod, access } from 'fs/promises';
|
import { access, chmod } from 'node:fs/promises';
|
||||||
import path from 'path';
|
import path from 'node:path';
|
||||||
import { platform, arch } from 'process';
|
import { arch, platform } from 'node:process';
|
||||||
import { pipeline } from 'stream/promises';
|
import { Readable } from 'node:stream';
|
||||||
import { Readable } from 'stream';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { execa, type ResultPromise } from 'execa';
|
import { execa, type ResultPromise } from 'execa';
|
||||||
|
import { GITHUB_API, OPENWEBUI, SILLYTAVERN } from '@/constants';
|
||||||
import { logError } from '@/utils/node/logging';
|
|
||||||
import { sendKoboldOutput, sendToRenderer } from '../window';
|
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
import { SILLYTAVERN, OPENWEBUI, GITHUB_API } from '@/constants';
|
|
||||||
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 { sendKoboldOutput, sendToRenderer } from '../window';
|
||||||
|
|
||||||
let activeTunnel: ResultPromise | null = null;
|
let activeTunnel: ResultPromise | null = null;
|
||||||
let tunnelUrl: string | null = null;
|
let tunnelUrl: string | null = null;
|
||||||
|
|
@ -36,16 +35,11 @@ const getCloudflaredAssetName = () => {
|
||||||
const getCloudflaredDownloadUrl = async () => {
|
const getCloudflaredDownloadUrl = async () => {
|
||||||
const response = await fetch(GITHUB_API.CLOUDFLARED_LATEST_RELEASE_URL);
|
const response = await fetch(GITHUB_API.CLOUDFLARED_LATEST_RELEASE_URL);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(`Failed to fetch latest cloudflared release: ${response.statusText}`);
|
||||||
`Failed to fetch latest cloudflared release: ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const release = (await response.json()) as { tag_name: string };
|
const release = (await response.json()) as { tag_name: string };
|
||||||
return GITHUB_API.getCloudflaredDownloadUrl(
|
return GITHUB_API.getCloudflaredDownloadUrl(release.tag_name, getCloudflaredAssetName());
|
||||||
release.tag_name,
|
|
||||||
getCloudflaredAssetName()
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadCloudflared = async (binPath: string) => {
|
const downloadCloudflared = async (binPath: string) => {
|
||||||
|
|
@ -57,11 +51,8 @@ const downloadCloudflared = async (binPath: string) => {
|
||||||
throw new Error(`Failed to download cloudflared: ${response.statusText}`);
|
throw new Error(`Failed to download cloudflared: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipeline(
|
// biome-ignore lint/suspicious/noExplicitAny: Node.js stream types are incompatible with web streams
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
await pipeline(Readable.fromWeb(response.body as any), createWriteStream(binPath));
|
||||||
Readable.fromWeb(response.body as any),
|
|
||||||
createWriteStream(binPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (platform !== 'win32') {
|
if (platform !== 'win32') {
|
||||||
await chmod(binPath, 0o755);
|
await chmod(binPath, 0o755);
|
||||||
|
|
@ -100,9 +91,7 @@ const waitForBackend = async (url: string, timeoutMs = 30000) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startTunnel = async (
|
export const startTunnel = async (frontendPreference: FrontendPreference = 'koboldcpp') => {
|
||||||
frontendPreference: FrontendPreference = 'koboldcpp'
|
|
||||||
) => {
|
|
||||||
if (activeTunnel) {
|
if (activeTunnel) {
|
||||||
return tunnelUrl;
|
return tunnelUrl;
|
||||||
}
|
}
|
||||||
|
|
@ -131,12 +120,7 @@ export const startTunnel = async (
|
||||||
await downloadCloudflared(bin);
|
await downloadCloudflared(bin);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tunnel = execa(bin, [
|
const tunnel = execa(bin, ['tunnel', '--url', tunnelTarget, '--no-autoupdate']);
|
||||||
'tunnel',
|
|
||||||
'--url',
|
|
||||||
tunnelTarget,
|
|
||||||
'--no-autoupdate',
|
|
||||||
]);
|
|
||||||
|
|
||||||
activeTunnel = tunnel;
|
activeTunnel = tunnel;
|
||||||
|
|
||||||
|
|
@ -204,9 +188,7 @@ export const startTunnel = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
tunnel.on('exit', (code: number, signal: string) => {
|
tunnel.on('exit', (code: number, signal: string) => {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(`Tunnel process exited (code: ${code}, signal: ${signal})`);
|
||||||
`Tunnel process exited (code: ${code}, signal: ${signal})`
|
|
||||||
);
|
|
||||||
activeTunnel = null;
|
activeTunnel = null;
|
||||||
tunnelUrl = null;
|
tunnelUrl = null;
|
||||||
sendToRenderer('tunnel-url-changed', null);
|
sendToRenderer('tunnel-url-changed', null);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { mem, cpuTemperature, currentLoad } from 'systeminformation';
|
import { spawn } from 'node:child_process';
|
||||||
import { BrowserWindow } from 'electron';
|
import { platform } from 'node:process';
|
||||||
import { platform } from 'process';
|
import type { BrowserWindow } from 'electron';
|
||||||
import { spawn } from 'child_process';
|
import { cpuTemperature, currentLoad, mem } from 'systeminformation';
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
import { getGPUData } from '@/utils/node/gpu';
|
||||||
|
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
||||||
import { detectGPU } from './hardware';
|
import { detectGPU } from './hardware';
|
||||||
import { tryExecute, safeExecute } from '@/utils/node/logging';
|
|
||||||
import { isTrayActive, updateMetrics } from './tray';
|
import { isTrayActive, updateMetrics } from './tray';
|
||||||
|
|
||||||
export interface CpuMetrics {
|
export interface CpuMetrics {
|
||||||
|
|
@ -115,9 +115,7 @@ async function collectAndSendCpuMetrics() {
|
||||||
try {
|
try {
|
||||||
const tempData = await cpuTemperature();
|
const tempData = await cpuTemperature();
|
||||||
metrics.temperature =
|
metrics.temperature =
|
||||||
tempData.main && tempData.main > 0
|
tempData.main && tempData.main > 0 ? Math.round(tempData.main) : undefined;
|
||||||
? Math.round(tempData.main)
|
|
||||||
: undefined;
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,10 +242,7 @@ export const openPerformanceManager = async () =>
|
||||||
(await safeExecute(async () => {
|
(await safeExecute(async () => {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'darwin': {
|
case 'darwin': {
|
||||||
const success = await tryLaunchCommand('open', [
|
const success = await tryLaunchCommand('open', ['-a', 'Activity Monitor']);
|
||||||
'-a',
|
|
||||||
'Activity Monitor',
|
|
||||||
]);
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return { success: true, app: 'Activity Monitor' };
|
return { success: true, app: 'Activity Monitor' };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { get, set, getInstallDir } from './config';
|
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT } from '@/constants/notepad';
|
||||||
import type { SavedNotepadState } from '@/types/electron';
|
import type { SavedNotepadState } from '@/types/electron';
|
||||||
import {
|
|
||||||
DEFAULT_NOTEPAD_POSITION,
|
|
||||||
DEFAULT_TAB_CONTENT,
|
|
||||||
} from '@/constants/notepad';
|
|
||||||
import { pathExists } from '@/utils/node/fs';
|
import { pathExists } from '@/utils/node/fs';
|
||||||
import { join } from 'path';
|
import { get, getInstallDir, set } from './config';
|
||||||
import { readFile, readdir, writeFile, unlink, rename } from 'fs/promises';
|
|
||||||
|
|
||||||
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
|
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
|
||||||
activeTabId: null,
|
activeTabId: null,
|
||||||
|
|
@ -41,10 +38,7 @@ export const renameTab = async (oldTitle: string, newTitle: string) => {
|
||||||
try {
|
try {
|
||||||
const notepadDir = getNotepadDir();
|
const notepadDir = getNotepadDir();
|
||||||
|
|
||||||
await rename(
|
await rename(join(notepadDir, `${oldTitle}.txt`), join(notepadDir, `${newTitle}.txt`));
|
||||||
join(notepadDir, `${oldTitle}.txt`),
|
|
||||||
join(notepadDir, `${newTitle}.txt`)
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,21 @@
|
||||||
import { spawn } from 'child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
import { join } from 'path';
|
import { spawn } from 'node:child_process';
|
||||||
import { on } from 'process';
|
import { join } from 'node:path';
|
||||||
|
import { on } from 'node:process';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import type { ChildProcess } from 'child_process';
|
|
||||||
|
|
||||||
import { logError } from '@/utils/node/logging';
|
|
||||||
import { sendKoboldOutput, sendToRenderer } from './window';
|
|
||||||
import { getInstallDir } from './config';
|
|
||||||
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
|
||||||
import { getUvEnvironment } from './dependencies';
|
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
|
import { logError } from '@/utils/node/logging';
|
||||||
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
|
import { getInstallDir } from './config';
|
||||||
|
import { getUvEnvironment } from './dependencies';
|
||||||
|
import { sendKoboldOutput, sendToRenderer } from './window';
|
||||||
|
|
||||||
let openWebUIProcess: ChildProcess | null = null;
|
let openWebUIProcess: ChildProcess | null = null;
|
||||||
|
|
||||||
const OPENWEBUI_VERSION = '0.6.41';
|
const OPENWEBUI_VERSION = '0.6.41';
|
||||||
const OPENWEBUI_BASE_ARGS = [
|
const OPENWEBUI_BASE_ARGS = ['--python', '3.11', `open-webui@${OPENWEBUI_VERSION}`, 'serve'];
|
||||||
'--python',
|
|
||||||
'3.11',
|
|
||||||
`open-webui@${OPENWEBUI_VERSION}`,
|
|
||||||
'serve',
|
|
||||||
];
|
|
||||||
|
|
||||||
on('SIGINT', () => {
|
on('SIGINT', () => {
|
||||||
void stopFrontend();
|
void stopFrontend();
|
||||||
|
|
@ -76,11 +70,7 @@ export async function startFrontend(args: string[]) {
|
||||||
name: 'openwebui',
|
name: 'openwebui',
|
||||||
port: OPENWEBUI.PORT,
|
port: OPENWEBUI.PORT,
|
||||||
};
|
};
|
||||||
const {
|
const { host: koboldHost, port: koboldPort, isImageMode } = parseKoboldConfig(args);
|
||||||
host: koboldHost,
|
|
||||||
port: koboldPort,
|
|
||||||
isImageMode,
|
|
||||||
} = parseKoboldConfig(args);
|
|
||||||
|
|
||||||
await stopFrontend();
|
await stopFrontend();
|
||||||
const appVersion = app.getVersion();
|
const appVersion = app.getVersion();
|
||||||
|
|
@ -92,11 +82,7 @@ export async function startFrontend(args: string[]) {
|
||||||
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||||
const proxyUrl = PROXY.URL;
|
const proxyUrl = PROXY.URL;
|
||||||
|
|
||||||
const openWebUIArgs = [
|
const openWebUIArgs = [...OPENWEBUI_BASE_ARGS, '--port', config.port.toString()];
|
||||||
...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)' : ''}...`
|
||||||
|
|
@ -143,16 +129,13 @@ export async function startFrontend(args: string[]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openWebUIProcess.on(
|
openWebUIProcess.on('exit', (code: number | null, signal: string | null) => {
|
||||||
'exit',
|
|
||||||
(code: number | null, signal: string | null) => {
|
|
||||||
const message = signal
|
const message = signal
|
||||||
? `Open WebUI terminated with signal ${signal}`
|
? `Open WebUI terminated with signal ${signal}`
|
||||||
: `Open WebUI exited with code ${code}`;
|
: `Open WebUI exited with code ${code}`;
|
||||||
sendKoboldOutput(message);
|
sendKoboldOutput(message);
|
||||||
openWebUIProcess = null;
|
openWebUIProcess = null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
openWebUIProcess.on('error', (error) => {
|
openWebUIProcess.on('error', (error) => {
|
||||||
logError('Open WebUI process error:', error);
|
logError('Open WebUI process error:', error);
|
||||||
|
|
@ -162,7 +145,7 @@ export async function startFrontend(args: string[]) {
|
||||||
|
|
||||||
await waitForOpenWebUIToStart();
|
await waitForOpenWebUIToStart();
|
||||||
|
|
||||||
sendKoboldOutput(`Open WebUI is ready and auto-configured!`);
|
sendKoboldOutput('Open WebUI is ready and auto-configured!');
|
||||||
sendKoboldOutput(`Access Open WebUI at: http://localhost:${config.port}`);
|
sendKoboldOutput(`Access Open WebUI at: http://localhost:${config.port}`);
|
||||||
sendKoboldOutput(`Text Generation: ${proxyUrl}/v1 (auto-configured)`);
|
sendKoboldOutput(`Text Generation: ${proxyUrl}/v1 (auto-configured)`);
|
||||||
if (isImageMode) {
|
if (isImageMode) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { platform } from 'process';
|
import { platform } from 'node:process';
|
||||||
import { detectGPU } from './hardware';
|
|
||||||
import { safeExecute } from '@/utils/node/logging';
|
import { safeExecute } from '@/utils/node/logging';
|
||||||
|
import { detectGPU } from './hardware';
|
||||||
|
|
||||||
export interface PlatformGPUInfo {
|
export interface PlatformGPUInfo {
|
||||||
hasAMD: boolean;
|
hasAMD: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,22 @@
|
||||||
import { spawn } from 'child_process';
|
import type { ChildProcess } from 'node:child_process';
|
||||||
import { createServer, request, type Server } from 'http';
|
import { spawn } from 'node:child_process';
|
||||||
import { join } from 'path';
|
import { createServer, request, type Server } from 'node:http';
|
||||||
import { platform, on } from 'process';
|
import { join } from 'node:path';
|
||||||
import type { ChildProcess } from 'child_process';
|
import { on, platform } from 'node:process';
|
||||||
|
import { SERVER_READY_SIGNALS, SILLYTAVERN } from '@/constants';
|
||||||
import { logError, tryExecute } from '@/utils/node/logging';
|
|
||||||
import { sendKoboldOutput, sendToRenderer } from './window';
|
|
||||||
import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
|
|
||||||
import { PROXY } from '@/constants/proxy';
|
import { PROXY } from '@/constants/proxy';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
|
||||||
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 { getNodeEnvironment } from './dependencies';
|
import { logError, tryExecute } from '@/utils/node/logging';
|
||||||
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
import { getInstallDir } from './config';
|
import { getInstallDir } from './config';
|
||||||
|
import { getNodeEnvironment } from './dependencies';
|
||||||
|
import { sendKoboldOutput, sendToRenderer } from './window';
|
||||||
|
|
||||||
let sillyTavernProcess: ChildProcess | null = null;
|
let sillyTavernProcess: ChildProcess | null = null;
|
||||||
let proxyServer: Server | null = null;
|
let proxyServer: Server | null = null;
|
||||||
|
|
||||||
const SILLYTAVERN_BASE_ARGS = [
|
const SILLYTAVERN_BASE_ARGS = ['--listen', '--browserLaunchEnabled', 'false', '--disableCsrf'];
|
||||||
'--listen',
|
|
||||||
'--browserLaunchEnabled',
|
|
||||||
'false',
|
|
||||||
'--disableCsrf',
|
|
||||||
];
|
|
||||||
|
|
||||||
on('SIGINT', () => {
|
on('SIGINT', () => {
|
||||||
void stopFrontend();
|
void stopFrontend();
|
||||||
|
|
@ -34,8 +28,7 @@ on('SIGTERM', () => {
|
||||||
|
|
||||||
const getSillyTavernDataDir = () => join(getInstallDir(), 'sillytavern-data');
|
const getSillyTavernDataDir = () => join(getInstallDir(), 'sillytavern-data');
|
||||||
|
|
||||||
const getSillyTavernInstallDir = () =>
|
const getSillyTavernInstallDir = () => join(getInstallDir(), 'sillytavern-server');
|
||||||
join(getInstallDir(), 'sillytavern-server');
|
|
||||||
|
|
||||||
const getSillyTavernServerPath = () =>
|
const getSillyTavernServerPath = () =>
|
||||||
join(getSillyTavernInstallDir(), 'node_modules', 'sillytavern', 'server.js');
|
join(getSillyTavernInstallDir(), 'node_modules', 'sillytavern', 'server.js');
|
||||||
|
|
@ -57,18 +50,14 @@ async function ensureSillyTavernInstalled() {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const rmCmd = platform === 'win32' ? 'rmdir' : 'rm';
|
const rmCmd = platform === 'win32' ? 'rmdir' : 'rm';
|
||||||
const rmArgs =
|
const rmArgs =
|
||||||
platform === 'win32'
|
platform === 'win32' ? ['/s', '/q', nodeModulesPath] : ['-rf', nodeModulesPath];
|
||||||
? ['/s', '/q', nodeModulesPath]
|
|
||||||
: ['-rf', nodeModulesPath];
|
|
||||||
|
|
||||||
spawn(rmCmd, rmArgs, {
|
spawn(rmCmd, rmArgs, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
shell: true,
|
shell: true,
|
||||||
})
|
})
|
||||||
.on('exit', (code) =>
|
.on('exit', (code) =>
|
||||||
code === 0
|
code === 0 ? resolve() : reject(new Error(`Failed with code ${code}`))
|
||||||
? resolve()
|
|
||||||
: reject(new Error(`Failed with code ${code}`))
|
|
||||||
)
|
)
|
||||||
.on('error', reject);
|
.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
@ -195,9 +184,7 @@ async function ensureSillyTavernSettings() {
|
||||||
|
|
||||||
await terminateProcess(initProcess);
|
await terminateProcess(initProcess);
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput('SillyTavern settings should now be generated');
|
||||||
'SillyTavern settings should now be generated'
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -223,15 +210,14 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||||
|
|
||||||
if (await pathExists(configPath)) {
|
if (await pathExists(configPath)) {
|
||||||
try {
|
try {
|
||||||
const existingSettings =
|
const existingSettings = await readJsonFile<Record<string, unknown>>(configPath);
|
||||||
await readJsonFile<Record<string, unknown>>(configPath);
|
|
||||||
|
|
||||||
if (existingSettings) {
|
if (existingSettings) {
|
||||||
settings = existingSettings;
|
settings = existingSettings;
|
||||||
sendKoboldOutput(`Loaded existing SillyTavern settings`);
|
sendKoboldOutput('Loaded existing SillyTavern settings');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
sendKoboldOutput(`Could not read existing settings, creating new ones`);
|
sendKoboldOutput('Could not read existing settings, creating new ones');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,12 +227,8 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||||
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)
|
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 = {};
|
if (!textgenSettings.server_urls) textgenSettings.server_urls = {};
|
||||||
const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
|
const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
|
||||||
|
|
@ -255,14 +237,12 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||||
settings.main_api = 'textgenerationwebui';
|
settings.main_api = 'textgenerationwebui';
|
||||||
textgenSettings.type = 'koboldcpp';
|
textgenSettings.type = 'koboldcpp';
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(`Configured SillyTavern for text generation at ${proxyUrl}`);
|
||||||
`Configured SillyTavern for text generation at ${proxyUrl}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isImageMode) {
|
if (isImageMode) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
`Image generation mode detected. Configure SillyTavern manually:\n` +
|
'Image generation mode detected. Configure SillyTavern manually:\n' +
|
||||||
`1. Open SillyTavern Settings (top-right gear icon)\n` +
|
'1. Open SillyTavern Settings (top-right gear icon)\n' +
|
||||||
`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` +
|
||||||
|
|
@ -272,13 +252,11 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
|
||||||
|
|
||||||
await writeJsonFile(configPath, settings);
|
await writeJsonFile(configPath, settings);
|
||||||
|
|
||||||
sendKoboldOutput(`SillyTavern configuration updated successfully!`);
|
sendKoboldOutput('SillyTavern configuration updated successfully!');
|
||||||
}, 'Failed to setup SillyTavern config');
|
}, 'Failed to setup SillyTavern config');
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput('Failed to configure SillyTavern. Check logs for details.');
|
||||||
`Failed to configure SillyTavern. Check logs for details.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -352,15 +330,13 @@ export async function startFrontend(args: string[]) {
|
||||||
|
|
||||||
await stopFrontend();
|
await stopFrontend();
|
||||||
|
|
||||||
sendKoboldOutput(`Preparing SillyTavern to connect via proxy...`);
|
sendKoboldOutput('Preparing SillyTavern to connect via proxy...');
|
||||||
|
|
||||||
await ensureSillyTavernInstalled();
|
await ensureSillyTavernInstalled();
|
||||||
await ensureSillyTavernSettings();
|
await ensureSillyTavernSettings();
|
||||||
await setupSillyTavernConfig(isImageMode);
|
await setupSillyTavernConfig(isImageMode);
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(`Starting ${config.name} frontend on port ${config.port}...`);
|
||||||
`Starting ${config.name} frontend on port ${config.port}...`
|
|
||||||
);
|
|
||||||
|
|
||||||
const sillyTavernDataDir = getSillyTavernDataDir();
|
const sillyTavernDataDir = getSillyTavernDataDir();
|
||||||
|
|
||||||
|
|
@ -386,16 +362,13 @@ export async function startFrontend(args: string[]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sillyTavernProcess.on(
|
sillyTavernProcess.on('exit', (code: number | null, signal: string | null) => {
|
||||||
'exit',
|
|
||||||
(code: number | null, signal: string | null) => {
|
|
||||||
const message = signal
|
const message = signal
|
||||||
? `SillyTavern terminated with signal ${signal}`
|
? `SillyTavern terminated with signal ${signal}`
|
||||||
: `SillyTavern exited with code ${code}`;
|
: `SillyTavern exited with code ${code}`;
|
||||||
sendKoboldOutput(message);
|
sendKoboldOutput(message);
|
||||||
sillyTavernProcess = null;
|
sillyTavernProcess = null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
sillyTavernProcess.on('error', (error) => {
|
sillyTavernProcess.on('error', (error) => {
|
||||||
logError('SillyTavern process error:', error);
|
logError('SillyTavern process error:', error);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createServer, Server } from 'http';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { readFile } from 'fs/promises';
|
import { createServer, type Server } from 'node:http';
|
||||||
import { join, normalize, resolve as resolvePath, sep } from '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';
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const startStaticServer = (distPath: string) =>
|
||||||
server = createServer(
|
server = createServer(
|
||||||
(req, res) =>
|
(req, res) =>
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const requestPath = req.url === '/' ? 'index.html' : req.url!;
|
const requestPath = req.url === '/' ? 'index.html' : (req.url ?? 'index.html');
|
||||||
const normalizedPath = normalize(join(distPath, requestPath));
|
const normalizedPath = normalize(join(distPath, requestPath));
|
||||||
const resolvedDistPath = resolvePath(distPath);
|
const resolvedDistPath = resolvePath(distPath);
|
||||||
|
|
||||||
|
|
@ -32,8 +32,7 @@ export const startStaticServer = (distPath: string) =>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await readFile(safeFilePath);
|
const content = await readFile(safeFilePath);
|
||||||
const contentType =
|
const contentType = lookup(safeFilePath) || 'application/octet-stream';
|
||||||
lookup(safeFilePath) || 'application/octet-stream';
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': contentType });
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
res.end(content);
|
res.end(content);
|
||||||
|
|
@ -45,7 +44,7 @@ export const startStaticServer = (distPath: string) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
server.listen(0, 'localhost', () => {
|
server.listen(0, 'localhost', () => {
|
||||||
const address = server!.address();
|
const address = server?.address();
|
||||||
if (address && typeof address !== 'string') {
|
if (address && typeof address !== 'string') {
|
||||||
serverPort = address.port;
|
serverPort = address.port;
|
||||||
resolve(`http://localhost:${serverPort}`);
|
resolve(`http://localhost:${serverPort}`);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import {
|
import { join } from 'node:path';
|
||||||
app,
|
import { platform, resourcesPath } from 'node:process';
|
||||||
Tray,
|
import { app, Menu, type MenuItemConstructorOptions, nativeImage, Tray } from 'electron';
|
||||||
Menu,
|
|
||||||
nativeImage,
|
|
||||||
MenuItemConstructorOptions,
|
|
||||||
} from 'electron';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { platform, resourcesPath } from 'process';
|
|
||||||
import { getEnableSystemTray } from './config';
|
|
||||||
import { getMainWindow } from './window';
|
|
||||||
import type { CpuMetrics, MemoryMetrics, GpuMetrics } from './monitoring';
|
|
||||||
import type { Screen } from '@/types';
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
import type { Screen } from '@/types';
|
||||||
import { stripFileExtension } from '@/utils/format';
|
import { stripFileExtension } from '@/utils/format';
|
||||||
|
import { getEnableSystemTray } from './config';
|
||||||
|
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from './monitoring';
|
||||||
|
import { getMainWindow } from './window';
|
||||||
|
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
let currentMetrics: {
|
let currentMetrics: {
|
||||||
|
|
@ -82,11 +76,7 @@ function showAndFocusWindow() {
|
||||||
function buildTooltipText() {
|
function buildTooltipText() {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
if (
|
if (appState.monitoringEnabled && currentMetrics.cpu && currentMetrics.memory) {
|
||||||
appState.monitoringEnabled &&
|
|
||||||
currentMetrics.cpu &&
|
|
||||||
currentMetrics.memory
|
|
||||||
) {
|
|
||||||
const metrics: string[] = [];
|
const metrics: string[] = [];
|
||||||
|
|
||||||
const cpuText = `CPU: ${currentMetrics.cpu.usage}%${currentMetrics.cpu.temperature ? ` • ${currentMetrics.cpu.temperature}°C` : ''}`;
|
const cpuText = `CPU: ${currentMetrics.cpu.usage}%${currentMetrics.cpu.temperature ? ` • ${currentMetrics.cpu.temperature}°C` : ''}`;
|
||||||
|
|
@ -97,13 +87,12 @@ function buildTooltipText() {
|
||||||
|
|
||||||
if (currentMetrics.gpu?.gpus) {
|
if (currentMetrics.gpu?.gpus) {
|
||||||
currentMetrics.gpu.gpus.forEach((gpu, index) => {
|
currentMetrics.gpu.gpus.forEach((gpu, index) => {
|
||||||
const gpuLabel =
|
const gpuLabel = (currentMetrics.gpu?.gpus?.length ?? 0) > 1 ? `GPU ${index + 1}` : 'GPU';
|
||||||
currentMetrics.gpu!.gpus.length > 1 ? `GPU ${index + 1}` : 'GPU';
|
|
||||||
const gpuText = `${gpuLabel}: ${gpu.usage}%${gpu.temperature ? ` • ${gpu.temperature}°C` : ''}`;
|
const gpuText = `${gpuLabel}: ${gpu.usage}%${gpu.temperature ? ` • ${gpu.temperature}°C` : ''}`;
|
||||||
metrics.push(gpuText);
|
metrics.push(gpuText);
|
||||||
|
|
||||||
const vramLabel =
|
const vramLabel =
|
||||||
currentMetrics.gpu!.gpus.length > 1 ? `VRAM ${index + 1}` : 'VRAM';
|
(currentMetrics.gpu?.gpus?.length ?? 0) > 1 ? `VRAM ${index + 1}` : 'VRAM';
|
||||||
const vramText = `${vramLabel}: ${gpu.memoryUsage}% • ${gpu.memoryUsed.toFixed(2)} GB`;
|
const vramText = `${vramLabel}: ${gpu.memoryUsage}% • ${gpu.memoryUsed.toFixed(2)} GB`;
|
||||||
metrics.push(vramText);
|
metrics.push(vramText);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import { BrowserWindow, app, shell, screen, Menu, clipboard } from 'electron';
|
import { join } from 'node:path';
|
||||||
import { join } from 'path';
|
import { stripVTControlCharacters } from 'node:util';
|
||||||
import { stripVTControlCharacters } from 'util';
|
import type { BrowserWindowConstructorOptions } from 'electron';
|
||||||
|
import { app, BrowserWindow, clipboard, Menu, screen, shell } from 'electron';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
||||||
import { isDevelopment } from '@/utils/node/environment';
|
import { isDevelopment } from '@/utils/node/environment';
|
||||||
import { startStaticServer } from './static-server';
|
|
||||||
import {
|
import {
|
||||||
getBackgroundColor,
|
getBackgroundColor,
|
||||||
|
getEnableSystemTray,
|
||||||
getWindowBounds,
|
getWindowBounds,
|
||||||
set as setConfig,
|
set as setConfig,
|
||||||
WindowBounds,
|
type WindowBounds,
|
||||||
getEnableSystemTray,
|
|
||||||
} from './config';
|
} from './config';
|
||||||
|
import { startStaticServer } from './static-server';
|
||||||
import { isTrayActive } from './tray';
|
import { isTrayActive } from './tray';
|
||||||
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
|
||||||
import type { BrowserWindowConstructorOptions } from 'electron';
|
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
|
@ -23,10 +23,7 @@ export async function createMainWindow(options?: { startHidden?: boolean }) {
|
||||||
|
|
||||||
const defaultWidth = 800;
|
const defaultWidth = 800;
|
||||||
const minHeight = 600;
|
const minHeight = 600;
|
||||||
const defaultHeight = Math.max(
|
const defaultHeight = Math.max(minHeight, Math.min(Math.floor(size.height * 0.75), 1000));
|
||||||
minHeight,
|
|
||||||
Math.min(Math.floor(size.height * 0.75), 1000)
|
|
||||||
);
|
|
||||||
|
|
||||||
const windowOptions = {
|
const windowOptions = {
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
|
|
@ -61,12 +58,8 @@ export async function createMainWindow(options?: { startHidden?: boolean }) {
|
||||||
windowOptions.x = savedBounds.x;
|
windowOptions.x = savedBounds.x;
|
||||||
windowOptions.y = savedBounds.y;
|
windowOptions.y = savedBounds.y;
|
||||||
} else {
|
} else {
|
||||||
windowOptions.x = Math.floor(
|
windowOptions.x = Math.floor((size.width - (savedBounds.width || defaultWidth)) / 2);
|
||||||
(size.width - (savedBounds.width || defaultWidth)) / 2
|
windowOptions.y = Math.floor((size.height - (savedBounds.height || defaultHeight)) / 2);
|
||||||
);
|
|
||||||
windowOptions.y = Math.floor(
|
|
||||||
(size.height - (savedBounds.height || defaultHeight)) / 2
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
windowOptions.x = Math.floor((size.width - defaultWidth) / 2);
|
windowOptions.x = Math.floor((size.width - defaultWidth) / 2);
|
||||||
|
|
@ -165,7 +158,6 @@ export async function createMainWindow(options?: { startHidden?: boolean }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupContextMenu(mainWindow: BrowserWindow) {
|
function setupContextMenu(mainWindow: BrowserWindow) {
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
mainWindow.webContents.on('context-menu', (_, params) => {
|
mainWindow.webContents.on('context-menu', (_, params) => {
|
||||||
const hasLinkURL = !!params.linkURL;
|
const hasLinkURL = !!params.linkURL;
|
||||||
const hasSelection = !!params.selectionText;
|
const hasSelection = !!params.selectionText;
|
||||||
|
|
@ -179,8 +171,7 @@ function setupContextMenu(mainWindow: BrowserWindow) {
|
||||||
const canSelectAll = isEditable || params.mediaType === 'none';
|
const canSelectAll = isEditable || params.mediaType === 'none';
|
||||||
const canUndo = isEditable && params.editFlags?.canUndo;
|
const canUndo = isEditable && params.editFlags?.canUndo;
|
||||||
const canRedo = isEditable && params.editFlags?.canRedo;
|
const canRedo = isEditable && params.editFlags?.canRedo;
|
||||||
const hasEditOperations =
|
const hasEditOperations = canCut || canCopy || canPaste || canSelectAll || canUndo || canRedo;
|
||||||
canCut || canCopy || canPaste || canSelectAll || canUndo || canRedo;
|
|
||||||
|
|
||||||
const menuItems = [];
|
const menuItems = [];
|
||||||
|
|
||||||
|
|
@ -211,9 +202,7 @@ function setupContextMenu(mainWindow: BrowserWindow) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
label: 'Add to Dictionary',
|
label: 'Add to Dictionary',
|
||||||
click: () => {
|
click: () => {
|
||||||
mainWindow.webContents.session.addWordToSpellCheckerDictionary(
|
mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
|
||||||
params.misspelledWord
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -234,10 +223,7 @@ function setupContextMenu(mainWindow: BrowserWindow) {
|
||||||
menuItems.push({ label: 'Redo', role: 'redo' as const });
|
menuItems.push({ label: 'Redo', role: 'redo' as const });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if ((canUndo || canRedo) && (canCut || canCopy || canPaste || canSelectAll)) {
|
||||||
(canUndo || canRedo) &&
|
|
||||||
(canCut || canCopy || canPaste || canSelectAll)
|
|
||||||
) {
|
|
||||||
menuItems.push({ type: 'separator' as const });
|
menuItems.push({ type: 'separator' as const });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,49 @@
|
||||||
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
|
import { contextBridge, type IpcRendererEvent, ipcRenderer } from 'electron';
|
||||||
|
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
|
||||||
import type {
|
import type {
|
||||||
KoboldAPI,
|
|
||||||
AppAPI,
|
AppAPI,
|
||||||
ConfigAPI,
|
ConfigAPI,
|
||||||
LogsAPI,
|
|
||||||
DependenciesAPI,
|
DependenciesAPI,
|
||||||
|
KoboldAPI,
|
||||||
|
LogsAPI,
|
||||||
MonitoringAPI,
|
MonitoringAPI,
|
||||||
UpdaterAPI,
|
|
||||||
NotepadAPI,
|
NotepadAPI,
|
||||||
|
UpdaterAPI,
|
||||||
} from '@/types/electron';
|
} from '@/types/electron';
|
||||||
import type {
|
|
||||||
CpuMetrics,
|
|
||||||
MemoryMetrics,
|
|
||||||
GpuMetrics,
|
|
||||||
} from '@/main/modules/monitoring';
|
|
||||||
import type { KoboldCrashInfo } from '@/types/ipc';
|
import type { KoboldCrashInfo } from '@/types/ipc';
|
||||||
|
|
||||||
const koboldAPI: KoboldAPI = {
|
const koboldAPI: KoboldAPI = {
|
||||||
getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
|
getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
|
||||||
getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'),
|
getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'),
|
||||||
setCurrentBackend: (backend) =>
|
setCurrentBackend: (backend) => ipcRenderer.invoke('kobold:setCurrentBackend', backend),
|
||||||
ipcRenderer.invoke('kobold:setCurrentBackend', backend),
|
|
||||||
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
|
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
|
||||||
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
|
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
|
||||||
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
|
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
|
||||||
detectGPUCapabilities: () =>
|
detectGPUCapabilities: () => ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
||||||
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
|
||||||
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
||||||
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
|
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
|
||||||
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
||||||
detectAccelerationSupport: () =>
|
detectAccelerationSupport: () => ipcRenderer.invoke('kobold:detectAccelerationSupport'),
|
||||||
ipcRenderer.invoke('kobold:detectAccelerationSupport'),
|
|
||||||
getAvailableAccelerations: (includeDisabled) =>
|
getAvailableAccelerations: (includeDisabled) =>
|
||||||
ipcRenderer.invoke('kobold:getAvailableAccelerations', includeDisabled),
|
ipcRenderer.invoke('kobold:getAvailableAccelerations', includeDisabled),
|
||||||
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
||||||
selectInstallDirectory: () =>
|
selectInstallDirectory: () => ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
downloadRelease: (asset, options) => ipcRenderer.invoke('kobold:downloadRelease', asset, options),
|
||||||
downloadRelease: (asset, options) =>
|
deleteRelease: (binaryPath) => ipcRenderer.invoke('kobold:deleteRelease', binaryPath),
|
||||||
ipcRenderer.invoke('kobold:downloadRelease', asset, options),
|
|
||||||
deleteRelease: (binaryPath) =>
|
|
||||||
ipcRenderer.invoke('kobold:deleteRelease', binaryPath),
|
|
||||||
launchKoboldCpp: (args, preLaunchCommands) =>
|
launchKoboldCpp: (args, preLaunchCommands) =>
|
||||||
ipcRenderer.invoke('kobold:launchKoboldCpp', args, preLaunchCommands),
|
ipcRenderer.invoke('kobold:launchKoboldCpp', args, preLaunchCommands),
|
||||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
||||||
saveConfigFile: (configName, configData) =>
|
saveConfigFile: (configName, configData) =>
|
||||||
ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
|
ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
|
||||||
deleteConfigFile: (configName) =>
|
deleteConfigFile: (configName) => ipcRenderer.invoke('kobold:deleteConfigFile', configName),
|
||||||
ipcRenderer.invoke('kobold:deleteConfigFile', configName),
|
|
||||||
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
|
||||||
setSelectedConfig: (configName) =>
|
setSelectedConfig: (configName) => ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
||||||
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
|
parseConfigFile: (filePath) => ipcRenderer.invoke('kobold:parseConfigFile', filePath),
|
||||||
parseConfigFile: (filePath) =>
|
selectModelFile: (title) => ipcRenderer.invoke('kobold:selectModelFile', title),
|
||||||
ipcRenderer.invoke('kobold:parseConfigFile', filePath),
|
|
||||||
selectModelFile: (title) =>
|
|
||||||
ipcRenderer.invoke('kobold:selectModelFile', title),
|
|
||||||
importLocalBackend: () => ipcRenderer.invoke('kobold:importLocalBackend'),
|
importLocalBackend: () => ipcRenderer.invoke('kobold:importLocalBackend'),
|
||||||
getLocalModels: (paramType) =>
|
getLocalModels: (paramType) => ipcRenderer.invoke('kobold:getLocalModels', paramType),
|
||||||
ipcRenderer.invoke('kobold:getLocalModels', paramType),
|
analyzeModel: (filePath) => ipcRenderer.invoke('kobold:analyzeModel', filePath),
|
||||||
analyzeModel: (filePath) =>
|
calculateOptimalLayers: (modelPath, contextSize, availableVramGB, flashAttention, acceleration) =>
|
||||||
ipcRenderer.invoke('kobold:analyzeModel', filePath),
|
|
||||||
calculateOptimalLayers: (
|
|
||||||
modelPath,
|
|
||||||
contextSize,
|
|
||||||
availableVramGB,
|
|
||||||
flashAttention,
|
|
||||||
acceleration
|
|
||||||
) =>
|
|
||||||
ipcRenderer.invoke(
|
ipcRenderer.invoke(
|
||||||
'kobold:calculateOptimalLayers',
|
'kobold:calculateOptimalLayers',
|
||||||
modelPath,
|
modelPath,
|
||||||
|
|
@ -76,8 +54,7 @@ const koboldAPI: KoboldAPI = {
|
||||||
),
|
),
|
||||||
stopKoboldCpp: () => void ipcRenderer.invoke('kobold:stopKoboldCpp'),
|
stopKoboldCpp: () => void ipcRenderer.invoke('kobold:stopKoboldCpp'),
|
||||||
onDownloadProgress: (callback) => {
|
onDownloadProgress: (callback) => {
|
||||||
const handler = (_: IpcRendererEvent, progress: number) =>
|
const handler = (_: IpcRendererEvent, progress: number) => callback(progress);
|
||||||
callback(progress);
|
|
||||||
ipcRenderer.on('download-progress', handler);
|
ipcRenderer.on('download-progress', handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -109,8 +86,7 @@ const koboldAPI: KoboldAPI = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onKoboldCrashed: (callback) => {
|
onKoboldCrashed: (callback) => {
|
||||||
const handler = (_: IpcRendererEvent, crashInfo: KoboldCrashInfo) =>
|
const handler = (_: IpcRendererEvent, crashInfo: KoboldCrashInfo) => callback(crashInfo);
|
||||||
callback(crashInfo);
|
|
||||||
ipcRenderer.on('kobold-crashed', handler);
|
ipcRenderer.on('kobold-crashed', handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -148,15 +124,11 @@ const appAPI: AppAPI = {
|
||||||
getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'),
|
getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'),
|
||||||
setZoomLevel: async (level) => ipcRenderer.invoke('app:setZoomLevel', level),
|
setZoomLevel: async (level) => ipcRenderer.invoke('app:setZoomLevel', level),
|
||||||
getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'),
|
getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'),
|
||||||
setColorScheme: async (colorScheme) =>
|
setColorScheme: async (colorScheme) => ipcRenderer.invoke('app:setColorScheme', colorScheme),
|
||||||
ipcRenderer.invoke('app:setColorScheme', colorScheme),
|
|
||||||
getEnableSystemTray: () => ipcRenderer.invoke('app:getEnableSystemTray'),
|
getEnableSystemTray: () => ipcRenderer.invoke('app:getEnableSystemTray'),
|
||||||
setEnableSystemTray: (enabled) =>
|
setEnableSystemTray: (enabled) => ipcRenderer.invoke('app:setEnableSystemTray', enabled),
|
||||||
ipcRenderer.invoke('app:setEnableSystemTray', enabled),
|
getStartMinimizedToTray: () => ipcRenderer.invoke('app:getStartMinimizedToTray'),
|
||||||
getStartMinimizedToTray: () =>
|
setStartMinimizedToTray: (enabled) => ipcRenderer.invoke('app:setStartMinimizedToTray', enabled),
|
||||||
ipcRenderer.invoke('app:getStartMinimizedToTray'),
|
|
||||||
setStartMinimizedToTray: (enabled) =>
|
|
||||||
ipcRenderer.invoke('app:setStartMinimizedToTray', enabled),
|
|
||||||
updateTrayState: (state) => ipcRenderer.invoke('app:updateTrayState', state),
|
updateTrayState: (state) => ipcRenderer.invoke('app:updateTrayState', state),
|
||||||
onTrayEject: (callback) => {
|
onTrayEject: (callback) => {
|
||||||
const handler = () => callback();
|
const handler = () => callback();
|
||||||
|
|
@ -166,8 +138,7 @@ const appAPI: AppAPI = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
|
||||||
openPerformanceManager: () =>
|
openPerformanceManager: () => ipcRenderer.invoke('app:openPerformanceManager'),
|
||||||
ipcRenderer.invoke('app:openPerformanceManager'),
|
|
||||||
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
|
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
|
||||||
downloadUpdate: () => ipcRenderer.invoke('app:downloadUpdate'),
|
downloadUpdate: () => ipcRenderer.invoke('app:downloadUpdate'),
|
||||||
quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'),
|
quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'),
|
||||||
|
|
@ -184,8 +155,7 @@ const appAPI: AppAPI = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onLineNumbersChanged: (callback) => {
|
onLineNumbersChanged: (callback) => {
|
||||||
const handler = (_: IpcRendererEvent, showLineNumbers: boolean) =>
|
const handler = (_: IpcRendererEvent, showLineNumbers: boolean) => callback(showLineNumbers);
|
||||||
callback(showLineNumbers);
|
|
||||||
|
|
||||||
ipcRenderer.on('line-numbers-changed', handler);
|
ipcRenderer.on('line-numbers-changed', handler);
|
||||||
|
|
||||||
|
|
@ -201,15 +171,13 @@ const configAPI: ConfigAPI = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const logsAPI: LogsAPI = {
|
const logsAPI: LogsAPI = {
|
||||||
logError: (message, error) =>
|
logError: (message, error) => ipcRenderer.send('logs:logError', message, error),
|
||||||
ipcRenderer.send('logs:logError', message, error),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dependenciesAPI: DependenciesAPI = {
|
const dependenciesAPI: DependenciesAPI = {
|
||||||
isUvAvailable: () => ipcRenderer.invoke('dependencies:isUvAvailable'),
|
isUvAvailable: () => ipcRenderer.invoke('dependencies:isUvAvailable'),
|
||||||
isNpxAvailable: () => ipcRenderer.invoke('dependencies:isNpxAvailable'),
|
isNpxAvailable: () => ipcRenderer.invoke('dependencies:isNpxAvailable'),
|
||||||
clearOpenWebUIData: () =>
|
clearOpenWebUIData: () => ipcRenderer.invoke('dependencies:clearOpenWebUIData'),
|
||||||
ipcRenderer.invoke('dependencies:clearOpenWebUIData'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const monitoringAPI: MonitoringAPI = {
|
const monitoringAPI: MonitoringAPI = {
|
||||||
|
|
@ -221,8 +189,7 @@ const monitoringAPI: MonitoringAPI = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onCpuMetrics: (callback) => {
|
onCpuMetrics: (callback) => {
|
||||||
const handler = (_: IpcRendererEvent, metrics: CpuMetrics) =>
|
const handler = (_: IpcRendererEvent, metrics: CpuMetrics) => callback(metrics);
|
||||||
callback(metrics);
|
|
||||||
ipcRenderer.on('cpu-metrics', handler);
|
ipcRenderer.on('cpu-metrics', handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -230,8 +197,7 @@ const monitoringAPI: MonitoringAPI = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onMemoryMetrics: (callback) => {
|
onMemoryMetrics: (callback) => {
|
||||||
const handler = (_: IpcRendererEvent, metrics: MemoryMetrics) =>
|
const handler = (_: IpcRendererEvent, metrics: MemoryMetrics) => callback(metrics);
|
||||||
callback(metrics);
|
|
||||||
ipcRenderer.on('memory-metrics', handler);
|
ipcRenderer.on('memory-metrics', handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -239,8 +205,7 @@ const monitoringAPI: MonitoringAPI = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onGpuMetrics: (callback) => {
|
onGpuMetrics: (callback) => {
|
||||||
const handler = (_: IpcRendererEvent, metrics: GpuMetrics) =>
|
const handler = (_: IpcRendererEvent, metrics: GpuMetrics) => callback(metrics);
|
||||||
callback(metrics);
|
|
||||||
ipcRenderer.on('gpu-metrics', handler);
|
ipcRenderer.on('gpu-metrics', handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -259,12 +224,9 @@ const updaterAPI: UpdaterAPI = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const notepadAPI: NotepadAPI = {
|
const notepadAPI: NotepadAPI = {
|
||||||
saveTabContent: (title, content) =>
|
saveTabContent: (title, content) => ipcRenderer.invoke('notepad:saveTabContent', title, content),
|
||||||
ipcRenderer.invoke('notepad:saveTabContent', title, content),
|
loadTabContent: (title) => ipcRenderer.invoke('notepad:loadTabContent', title),
|
||||||
loadTabContent: (title) =>
|
renameTab: (oldTitle, newTitle) => ipcRenderer.invoke('notepad:renameTab', oldTitle, newTitle),
|
||||||
ipcRenderer.invoke('notepad:loadTabContent', title),
|
|
||||||
renameTab: (oldTitle, newTitle) =>
|
|
||||||
ipcRenderer.invoke('notepad:renameTab', oldTitle, newTitle),
|
|
||||||
saveState: (state) => ipcRenderer.invoke('notepad:saveState', state),
|
saveState: (state) => ipcRenderer.invoke('notepad:saveState', state),
|
||||||
loadState: () => ipcRenderer.invoke('notepad:loadState'),
|
loadState: () => ipcRenderer.invoke('notepad:loadState'),
|
||||||
deleteTab: (title) => ipcRenderer.invoke('notepad:deleteTab', title),
|
deleteTab: (title) => ipcRenderer.invoke('notepad:deleteTab', title),
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue