mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-04 04:04:44 -07:00
seamless openwebui integration, --version CLI support
This commit is contained in:
parent
75d5bcc020
commit
2128916701
22 changed files with 685 additions and 308 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,3 +17,4 @@ clip_l.safetensors
|
||||||
t5xxl_fp8_e4m3fn.safetensors
|
t5xxl_fp8_e4m3fn.safetensors
|
||||||
flux1-kontext-dev-Q3_K_S.gguf
|
flux1-kontext-dev-Q3_K_S.gguf
|
||||||
gemma-3-4b-it.Q8_0.gguf
|
gemma-3-4b-it.Q8_0.gguf
|
||||||
|
.webui_secret_key
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -2,19 +2,20 @@
|
||||||
|
|
||||||
A desktop app to easily run Large Language Models locally.
|
A desktop app to easily run Large Language Models locally.
|
||||||
|
|
||||||
<img src="src/assets/icon.png" alt="Gerbil Icon" width="32" height="32">
|
<img src="src/assets/icon.png" alt="Gerbil Icon" width="32" height="32" />
|
||||||
|
|
||||||
<!-- markdownlint-enable MD033 -->
|
<!-- markdownlint-enable MD033 -->
|
||||||
|
|
||||||
## Core Features
|
## Core Features
|
||||||
|
|
||||||
- **Run LLMs locally** powered by [KoboldCpp](https://github.com/LostRuins/koboldcpp)
|
- **Run LLMs locally** powered by [KoboldCpp](https://github.com/LostRuins/koboldcpp) which itself is a highly modified fork of [llama.cpp](https://github.com/ggml-org/llama.cpp)
|
||||||
- **Cross-platform desktop app** - Native support for Windows, macOS, and Linux (including Wayland)
|
- **Cross-platform desktop app** - Native support for Windows, macOS, and Linux (including Wayland)
|
||||||
- **Automatic updates** - Download and keep your KoboldCpp binary up-to-date effortlessly
|
- **Automatic updates** - Download and keep your KoboldCpp binary up-to-date effortlessly
|
||||||
- **Smart process management** - Prevents runaway background processes and system resource waste
|
- **Smart process management** - Prevents runaway background processes and system resource waste
|
||||||
- **Optimized performance** - Automatically unpacks binaries for faster operation and reduced memory usage
|
- **Optimized performance** - Automatically unpacks binaries for faster operation and reduced memory usage
|
||||||
- **Image generation support** - Built-in presets for Flux and Chroma image generation workflows
|
- **Image generation support** - Built-in presets for Flux and Chroma image generation workflows
|
||||||
- **SillyTavern integration** - Seamlessly launch SillyTavern for advanced character interactions (requires [Node.js](https://nodejs.org/))
|
- **SillyTavern integration** - Seamlessly launch SillyTavern for advanced character interactions (requires [Node.js](https://nodejs.org/))
|
||||||
|
- **OpenWebUI integration** - Launch OpenWebUI for a modern web-based chat interface (requires [uv](https://docs.astral.sh/uv/getting-started/installation/))
|
||||||
- **Privacy-focused** - Everything runs locally on your machine, no data sent to external servers
|
- **Privacy-focused** - Everything runs locally on your machine, no data sent to external servers
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
@ -49,9 +50,6 @@ The AUR package automatically handles installation, desktop integration, and sys
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
### Download & Setup
|
### Download & Setup
|
||||||
|
|
||||||
<img src="screenshots/download.png" alt="Download Interface" width="600">
|
<img src="screenshots/download.png" alt="Download Interface" width="600">
|
||||||
|
|
@ -72,12 +70,13 @@ The AUR package automatically handles installation, desktop integration, and sys
|
||||||
|
|
||||||
<img src="screenshots/gen-img.png" alt="Image Generation" width="600">
|
<img src="screenshots/gen-img.png" alt="Image Generation" width="600">
|
||||||
|
|
||||||
### Seemless SillyTavern integration
|
### Seamless SillyTavern integration
|
||||||
|
|
||||||
<img src="screenshots/sillytavern.png" alt="SillyTavern integration" width="600">
|
<img src="screenshots/sillytavern.png" alt="SillyTavern integration" width="600">
|
||||||
|
|
||||||
</div>
|
### Seamless OpenWebUI integration
|
||||||
<!-- markdownlint-enable MD033 -->
|
|
||||||
|
<img src="screenshots/openwebui.png" alt="SillyTavern integration" width="600">
|
||||||
|
|
||||||
### Future features
|
### Future features
|
||||||
|
|
||||||
|
|
@ -147,3 +146,5 @@ You can use the CLI mode on Windows in exactly the same way as in the Linux/macO
|
||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL v3 License - see LICENSE file for details
|
AGPL v3 License - see LICENSE file for details
|
||||||
|
|
||||||
|
# test
|
||||||
|
|
|
||||||
17
package.json
17
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "0.9.6",
|
"version": "0.9.7",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
@ -56,11 +56,11 @@
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.42.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"electron": "^37.4.0",
|
"electron": "^38.0.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
|
|
@ -116,15 +116,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"extraResources": [
|
|
||||||
{
|
|
||||||
"from": "src/main/templates",
|
|
||||||
"to": "templates",
|
|
||||||
"filter": [
|
|
||||||
"**/*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mac": {
|
"mac": {
|
||||||
"compression": "maximum",
|
"compression": "maximum",
|
||||||
"category": "public.app-category.productivity",
|
"category": "public.app-category.productivity",
|
||||||
|
|
|
||||||
BIN
screenshots/openwebui.png
Normal file
BIN
screenshots/openwebui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
|
|
@ -165,12 +165,20 @@ export const TitleBar = ({
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: 'chat',
|
value: 'chat',
|
||||||
label:
|
label: (() => {
|
||||||
frontendPreference === 'sillytavern'
|
if (frontendPreference === 'sillytavern') {
|
||||||
? FRONTENDS.SILLYTAVERN
|
return FRONTENDS.SILLYTAVERN;
|
||||||
: isImageGenerationMode
|
}
|
||||||
? FRONTENDS.STABLE_UI
|
if (
|
||||||
: FRONTENDS.KOBOLDAI_LITE,
|
frontendPreference === 'openwebui' &&
|
||||||
|
!isImageGenerationMode
|
||||||
|
) {
|
||||||
|
return FRONTENDS.OPENWEBUI;
|
||||||
|
}
|
||||||
|
return isImageGenerationMode
|
||||||
|
? FRONTENDS.STABLE_UI
|
||||||
|
: FRONTENDS.KOBOLDAI_LITE;
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
{ value: 'terminal', label: 'Terminal' },
|
{ value: 'terminal', label: 'Terminal' },
|
||||||
{ value: 'eject', label: 'Eject' },
|
{ value: 'eject', label: 'Eject' },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { Box, Text, Stack } from '@mantine/core';
|
import { Box, Text, Stack } from '@mantine/core';
|
||||||
import { SILLYTAVERN, FRONTENDS, TITLEBAR_HEIGHT } from '@/constants';
|
import {
|
||||||
|
SILLYTAVERN,
|
||||||
|
OPENWEBUI,
|
||||||
|
FRONTENDS,
|
||||||
|
TITLEBAR_HEIGHT,
|
||||||
|
} from '@/constants';
|
||||||
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
|
|
||||||
|
|
@ -46,6 +51,9 @@ export const ServerTab = ({
|
||||||
if (frontendPreference === 'sillytavern') {
|
if (frontendPreference === 'sillytavern') {
|
||||||
iframeUrl = SILLYTAVERN.PROXY_URL;
|
iframeUrl = SILLYTAVERN.PROXY_URL;
|
||||||
title = FRONTENDS.SILLYTAVERN;
|
title = FRONTENDS.SILLYTAVERN;
|
||||||
|
} else if (frontendPreference === 'openwebui' && !isImageGenerationMode) {
|
||||||
|
iframeUrl = OPENWEBUI.URL;
|
||||||
|
title = FRONTENDS.OPENWEBUI;
|
||||||
} else {
|
} else {
|
||||||
iframeUrl = isImageGenerationMode ? `${serverUrl}/sdui` : serverUrl;
|
iframeUrl = isImageGenerationMode ? `${serverUrl}/sdui` : serverUrl;
|
||||||
title = isImageGenerationMode
|
title = isImageGenerationMode
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const TerminalTab = ({
|
||||||
onServerReady,
|
onServerReady,
|
||||||
frontendPreference = 'koboldcpp',
|
frontendPreference = 'koboldcpp',
|
||||||
}: TerminalTabProps) => {
|
}: TerminalTabProps) => {
|
||||||
const { host, port } = useLaunchConfigStore();
|
const { host, port, isImageGenerationMode } = useLaunchConfigStore();
|
||||||
const computedColorScheme = useComputedColorScheme('light', {
|
const computedColorScheme = useComputedColorScheme('light', {
|
||||||
getInitialValueInEffect: false,
|
getInitialValueInEffect: false,
|
||||||
});
|
});
|
||||||
|
|
@ -68,20 +68,22 @@ export const TerminalTab = ({
|
||||||
const serverHost = host || 'localhost';
|
const serverHost = host || 'localhost';
|
||||||
const serverPort = port || 5001;
|
const serverPort = port || 5001;
|
||||||
|
|
||||||
|
let signalToCheck: string = SERVER_READY_SIGNALS.KOBOLDCPP;
|
||||||
|
|
||||||
if (frontendPreference === 'sillytavern') {
|
if (frontendPreference === 'sillytavern') {
|
||||||
if (newData.includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN;
|
||||||
setTimeout(
|
} else if (
|
||||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
frontendPreference === 'openwebui' &&
|
||||||
1500
|
!isImageGenerationMode
|
||||||
);
|
) {
|
||||||
}
|
signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI;
|
||||||
} else {
|
}
|
||||||
if (newData.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
|
|
||||||
setTimeout(
|
if (newData.includes(signalToCheck)) {
|
||||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
setTimeout(
|
||||||
1500
|
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||||
);
|
1500
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +92,7 @@ export const TerminalTab = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [onServerReady, host, port, frontendPreference]);
|
}, [onServerReady, host, port, frontendPreference, isImageGenerationMode]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (viewportRef.current) {
|
if (viewportRef.current) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { tryExecute, safeExecute } from '@/utils/logger';
|
import { tryExecute, safeExecute } from '@/utils/logger';
|
||||||
import {
|
import {
|
||||||
Stack,
|
Stack,
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Button,
|
Button,
|
||||||
rem,
|
rem,
|
||||||
Select,
|
Select,
|
||||||
|
Box,
|
||||||
Anchor,
|
Anchor,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Folder, FolderOpen, Monitor } from 'lucide-react';
|
import { Folder, FolderOpen, Monitor } from 'lucide-react';
|
||||||
|
|
@ -15,43 +16,125 @@ import styles from '@/styles/layout.module.css';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
import { FRONTENDS } from '@/constants';
|
import { FRONTENDS } from '@/constants';
|
||||||
|
|
||||||
|
interface FrontendRequirement {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FrontendConfig {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
requirements?: FrontendRequirement[];
|
||||||
|
requirementCheck?: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
export const GeneralTab = () => {
|
||||||
const [installDir, setInstallDir] = useState<string>('');
|
const [installDir, setInstallDir] = useState<string>('');
|
||||||
const [FrontendPreference, setFrontendPreference] =
|
const [FrontendPreference, setFrontendPreference] =
|
||||||
useState<FrontendPreference>('koboldcpp');
|
useState<FrontendPreference>('koboldcpp');
|
||||||
const [isNpxAvailable, setIsNpxAvailable] = useState<boolean>(true);
|
const [frontendRequirements, setFrontendRequirements] = useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
const frontendConfigs: FrontendConfig[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'koboldcpp', label: 'KoboldCpp (Built-in)' },
|
||||||
|
{
|
||||||
|
value: 'sillytavern',
|
||||||
|
label: FRONTENDS.SILLYTAVERN,
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
id: 'nodejs',
|
||||||
|
name: 'Node.js',
|
||||||
|
url: 'https://nodejs.org/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requirementCheck: () => window.electronAPI.sillytavern.isNpxAvailable(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'openwebui',
|
||||||
|
label: FRONTENDS.OPENWEBUI,
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
id: 'uv',
|
||||||
|
name: 'uv',
|
||||||
|
url: 'https://docs.astral.sh/uv/getting-started/installation/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requirementCheck: () => window.electronAPI.openwebui.isUvAvailable(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkAllFrontendRequirements = useCallback(async () => {
|
||||||
|
const requirementResults = new Map<string, boolean>();
|
||||||
|
|
||||||
|
for (const config of frontendConfigs) {
|
||||||
|
if (config.requirementCheck) {
|
||||||
|
const isAvailable = await safeExecute(
|
||||||
|
config.requirementCheck,
|
||||||
|
`Failed to check requirements for ${config.label}:`
|
||||||
|
);
|
||||||
|
requirementResults.set(config.value, isAvailable ?? false);
|
||||||
|
} else {
|
||||||
|
requirementResults.set(config.value, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFrontendRequirements(requirementResults);
|
||||||
|
|
||||||
|
const currentFrontendConfig = frontendConfigs.find(
|
||||||
|
(config) => config.value === FrontendPreference
|
||||||
|
);
|
||||||
|
if (currentFrontendConfig && !requirementResults.get(FrontendPreference)) {
|
||||||
|
await tryExecute(
|
||||||
|
() => window.electronAPI.config.set('frontendPreference', 'koboldcpp'),
|
||||||
|
'Failed to reset frontend preference:'
|
||||||
|
);
|
||||||
|
setFrontendPreference('koboldcpp');
|
||||||
|
}
|
||||||
|
}, [frontendConfigs, FrontendPreference]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
await Promise.all([loadCurrentInstallDir(), loadFrontendPreference()]);
|
await Promise.all([
|
||||||
|
loadCurrentInstallDir(),
|
||||||
|
loadFrontendPreference(),
|
||||||
|
checkAllFrontendRequirements(),
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
initialize();
|
initialize();
|
||||||
}, []);
|
}, [checkAllFrontendRequirements]);
|
||||||
|
|
||||||
|
const getSelectedFrontendConfig = () =>
|
||||||
|
frontendConfigs.find((config) => config.value === FrontendPreference);
|
||||||
|
|
||||||
|
const getUnmetRequirements = () => {
|
||||||
|
const selectedConfig = getSelectedFrontendConfig();
|
||||||
|
if (!selectedConfig || !selectedConfig.requirements) return [];
|
||||||
|
|
||||||
|
const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true;
|
||||||
|
return isAvailable ? [] : selectedConfig.requirements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUnmetRequirementsForFrontend = (frontendValue: string) => {
|
||||||
|
const config = frontendConfigs.find((c) => c.value === frontendValue);
|
||||||
|
if (!config || !config.requirements) return [];
|
||||||
|
|
||||||
|
const isAvailable = frontendRequirements.get(frontendValue) ?? true;
|
||||||
|
return isAvailable ? [] : config.requirements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFrontendAvailable = (frontendValue: string) =>
|
||||||
|
frontendRequirements.get(frontendValue) ?? true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkNpxAvailability = async () => {
|
|
||||||
const available = await safeExecute(
|
|
||||||
() => window.electronAPI.sillytavern.isNpxAvailable(),
|
|
||||||
'Failed to check npx availability:'
|
|
||||||
);
|
|
||||||
|
|
||||||
const isAvailable = available ?? false;
|
|
||||||
setIsNpxAvailable(isAvailable);
|
|
||||||
|
|
||||||
if (!isAvailable && FrontendPreference === 'sillytavern') {
|
|
||||||
await tryExecute(
|
|
||||||
() =>
|
|
||||||
window.electronAPI.config.set('frontendPreference', 'koboldcpp'),
|
|
||||||
'Failed to reset frontend preference:'
|
|
||||||
);
|
|
||||||
setFrontendPreference('koboldcpp');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (FrontendPreference) {
|
if (FrontendPreference) {
|
||||||
checkNpxAvailability();
|
checkAllFrontendRequirements();
|
||||||
}
|
}
|
||||||
}, [FrontendPreference]);
|
}, [FrontendPreference, checkAllFrontendRequirements]);
|
||||||
|
|
||||||
const loadCurrentInstallDir = async () => {
|
const loadCurrentInstallDir = async () => {
|
||||||
const currentDir = await safeExecute(
|
const currentDir = await safeExecute(
|
||||||
|
|
@ -86,14 +169,15 @@ export const GeneralTab = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFrontendPreferenceChange = async (value: string | null) => {
|
const handleFrontendPreferenceChange = async (value: string | null) => {
|
||||||
if (!value || (value !== 'koboldcpp' && value !== 'sillytavern')) return;
|
if (!value || !['koboldcpp', 'sillytavern', 'openwebui'].includes(value))
|
||||||
|
return;
|
||||||
|
|
||||||
const success = await tryExecute(
|
const success = await tryExecute(
|
||||||
() => window.electronAPI.config.set('frontendPreference', value),
|
() => window.electronAPI.config.set('frontendPreference', value),
|
||||||
'Failed to save frontend preference:'
|
'Failed to save frontend preference:'
|
||||||
);
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
setFrontendPreference(value);
|
setFrontendPreference(value as FrontendPreference);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -130,43 +214,76 @@ export const GeneralTab = () => {
|
||||||
<Text fw={500} mb="sm">
|
<Text fw={500} mb="sm">
|
||||||
Frontend Interface
|
Frontend Interface
|
||||||
</Text>
|
</Text>
|
||||||
{isNpxAvailable && (
|
|
||||||
|
{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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isNpxAvailable && (
|
{getUnmetRequirements().length > 0 && (
|
||||||
<Text size="sm" c="red" mb="md">
|
<Text size="sm" c="red" mb="md">
|
||||||
Custom frontends require{' '}
|
{getSelectedFrontendConfig()?.label} requires{' '}
|
||||||
<Anchor
|
{getUnmetRequirements().map((req, index) => (
|
||||||
href="#"
|
<span key={req.id}>
|
||||||
onClick={(e) => {
|
<Anchor
|
||||||
e.preventDefault();
|
href="#"
|
||||||
window.electronAPI.app.openExternal('https://nodejs.org/');
|
onClick={(e) => {
|
||||||
}}
|
e.preventDefault();
|
||||||
c="red"
|
window.electronAPI.app.openExternal(req.url);
|
||||||
td="underline"
|
}}
|
||||||
>
|
c="red"
|
||||||
Node.js
|
td="underline"
|
||||||
</Anchor>{' '}
|
>
|
||||||
|
{req.name}
|
||||||
|
</Anchor>
|
||||||
|
{index < getUnmetRequirements().length - 1 ? ', ' : ''}
|
||||||
|
</span>
|
||||||
|
))}{' '}
|
||||||
to be installed on your system
|
to be installed on your system
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={FrontendPreference}
|
value={FrontendPreference}
|
||||||
onChange={handleFrontendPreferenceChange}
|
onChange={handleFrontendPreferenceChange}
|
||||||
disabled={!isNpxAvailable}
|
data={frontendConfigs.map((config) => ({
|
||||||
data={[
|
value: config.value,
|
||||||
{ value: 'koboldcpp', label: 'KoboldCpp (Built-in)' },
|
label: config.label,
|
||||||
{
|
disabled: !isFrontendAvailable(config.value),
|
||||||
value: 'sillytavern',
|
}))}
|
||||||
label: FRONTENDS.SILLYTAVERN,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box mt="sm">
|
||||||
|
{frontendConfigs
|
||||||
|
.filter((config) => !isFrontendAvailable(config.value))
|
||||||
|
.map((config) => {
|
||||||
|
const unmetReqs = getUnmetRequirementsForFrontend(config.value);
|
||||||
|
return (
|
||||||
|
<Text key={config.value} size="sm" c="orange" mb="xs">
|
||||||
|
{config.label} is disabled - requires{' '}
|
||||||
|
{unmetReqs.map((req, index) => (
|
||||||
|
<span key={req.id}>
|
||||||
|
<Anchor
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.electronAPI.app.openExternal(req.url);
|
||||||
|
}}
|
||||||
|
c="orange"
|
||||||
|
td="underline"
|
||||||
|
>
|
||||||
|
{req.name}
|
||||||
|
</Anchor>
|
||||||
|
{index < unmetReqs.length - 1 ? ', ' : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,10 @@ export const SILLYTAVERN = {
|
||||||
return `http://localhost:${this.PROXY_PORT}`;
|
return `http://localhost:${this.PROXY_PORT}`;
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const OPENWEBUI = {
|
||||||
|
PORT: 8080,
|
||||||
|
get URL() {
|
||||||
|
return `http://localhost:${this.PORT}`;
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export const TITLEBAR_HEIGHT = '2.5rem';
|
||||||
export const SERVER_READY_SIGNALS = {
|
export const SERVER_READY_SIGNALS = {
|
||||||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||||
SILLYTAVERN: 'SillyTavern is listening on',
|
SILLYTAVERN: 'SillyTavern is listening on',
|
||||||
|
OPENWEBUI: 'Waiting for application startup.',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export * from './defaults';
|
export * from './defaults';
|
||||||
|
|
@ -45,6 +46,7 @@ export const FRONTENDS = {
|
||||||
KOBOLDAI_LITE: 'KoboldAI Lite',
|
KOBOLDAI_LITE: 'KoboldAI Lite',
|
||||||
STABLE_UI: 'Stable UI',
|
STABLE_UI: 'Stable UI',
|
||||||
SILLYTAVERN: 'SillyTavern',
|
SILLYTAVERN: 'SillyTavern',
|
||||||
|
OPENWEBUI: 'Open WebUI',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ZOOM = {
|
export const ZOOM = {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { WindowManager } from '@/main/managers/WindowManager';
|
import { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { LogManager } from '@/main/managers/LogManager';
|
import { LogManager } from '@/main/managers/LogManager';
|
||||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
import { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||||
|
import { OpenWebUIManager } from '@/main/managers/OpenWebUIManager';
|
||||||
import { HardwareManager } from '@/main/managers/HardwareManager';
|
import { HardwareManager } from '@/main/managers/HardwareManager';
|
||||||
import { BinaryManager } from '@/main/managers/BinaryManager';
|
import { BinaryManager } from '@/main/managers/BinaryManager';
|
||||||
import { IPCHandlers } from '@/main/ipc';
|
import { IPCHandlers } from '@/main/ipc';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
|
||||||
import { homedir } from 'os';
|
|
||||||
import { ensureDir } from '@/utils/fs';
|
import { ensureDir } from '@/utils/fs';
|
||||||
import { getConfigDir } from '@/utils/path';
|
import { getConfigDir } from '@/utils/path';
|
||||||
|
|
||||||
|
|
@ -20,6 +18,7 @@ export class GerbilApp {
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private logManager: LogManager;
|
private logManager: LogManager;
|
||||||
private sillyTavernManager: SillyTavernManager;
|
private sillyTavernManager: SillyTavernManager;
|
||||||
|
private openWebUIManager: OpenWebUIManager;
|
||||||
private hardwareManager: HardwareManager;
|
private hardwareManager: HardwareManager;
|
||||||
private binaryManager: BinaryManager;
|
private binaryManager: BinaryManager;
|
||||||
private ipcHandlers: IPCHandlers;
|
private ipcHandlers: IPCHandlers;
|
||||||
|
|
@ -49,6 +48,12 @@ export class GerbilApp {
|
||||||
this.windowManager
|
this.windowManager
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.openWebUIManager = new OpenWebUIManager(
|
||||||
|
this.configManager,
|
||||||
|
this.logManager,
|
||||||
|
this.windowManager
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcHandlers = new IPCHandlers(
|
this.ipcHandlers = new IPCHandlers(
|
||||||
this.koboldManager,
|
this.koboldManager,
|
||||||
this.configManager,
|
this.configManager,
|
||||||
|
|
@ -56,32 +61,13 @@ export class GerbilApp {
|
||||||
this.binaryManager,
|
this.binaryManager,
|
||||||
this.logManager,
|
this.logManager,
|
||||||
this.sillyTavernManager,
|
this.sillyTavernManager,
|
||||||
|
this.openWebUIManager,
|
||||||
this.windowManager
|
this.windowManager
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultInstallDir(): string {
|
|
||||||
const platform = process.platform;
|
|
||||||
const home = homedir();
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case 'win32':
|
|
||||||
return join(home, PRODUCT_NAME);
|
|
||||||
case 'darwin':
|
|
||||||
return join(home, 'Applications', PRODUCT_NAME);
|
|
||||||
default:
|
|
||||||
return join(home, '.local', 'share', PRODUCT_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureInstallDirectory(): Promise<void> {
|
private async ensureInstallDirectory(): Promise<void> {
|
||||||
const installDir =
|
const installDir = this.configManager.getInstallDir();
|
||||||
this.configManager.getInstallDir() || this.getDefaultInstallDir();
|
|
||||||
|
|
||||||
if (!this.configManager.getInstallDir()) {
|
|
||||||
await this.configManager.setInstallDir(installDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureDir(installDir);
|
await ensureDir(installDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,10 +76,6 @@ export class GerbilApp {
|
||||||
await this.configManager.initialize();
|
await this.configManager.initialize();
|
||||||
await this.ensureInstallDirectory();
|
await this.ensureInstallDirectory();
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
app.setAppUserModelId('com.gerbil.app');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.windowManager.createMainWindow();
|
this.windowManager.createMainWindow();
|
||||||
this.ipcHandlers.setupHandlers();
|
this.ipcHandlers.setupHandlers();
|
||||||
|
|
||||||
|
|
@ -112,6 +94,7 @@ export class GerbilApp {
|
||||||
const cleanupPromises = [
|
const cleanupPromises = [
|
||||||
this.koboldManager.cleanup(),
|
this.koboldManager.cleanup(),
|
||||||
this.sillyTavernManager.cleanup(),
|
this.sillyTavernManager.cleanup(),
|
||||||
|
this.openWebUIManager.cleanup(),
|
||||||
];
|
];
|
||||||
|
|
||||||
const timeoutPromise = new Promise<void>((resolve) => {
|
const timeoutPromise = new Promise<void>((resolve) => {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,49 @@
|
||||||
const isCliMode = process.argv.includes('--cli');
|
import { readJsonFile } from '@/utils/fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
if (isCliMode) {
|
if (process.argv[1] === '--version') {
|
||||||
import('./cli')
|
(async () => {
|
||||||
.then(async (cliModule) => {
|
try {
|
||||||
const args = process.argv.slice(process.argv.indexOf('--cli') + 1);
|
const packageJsonPath = join(__dirname, '../../package.json');
|
||||||
const handler = new cliModule.LightweightCliHandler();
|
const packageJson = await readJsonFile<{ version: string }>(
|
||||||
try {
|
packageJsonPath
|
||||||
await handler.handleCliMode(args);
|
);
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('CLI mode error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('Failed to load CLI module:', error);
|
console.log(packageJson?.version || 'unknown');
|
||||||
process.exit(1);
|
} catch {
|
||||||
});
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('unknown');
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
})();
|
||||||
} else {
|
} else {
|
||||||
import('./gui').then((guiModule) => {
|
const isCliMode = process.argv.includes('--cli');
|
||||||
const app = new guiModule.GerbilApp();
|
|
||||||
app.initialize().catch((error: unknown) => {
|
if (isCliMode) {
|
||||||
// eslint-disable-next-line no-console
|
import('./cli')
|
||||||
console.error('Failed to initialize Gerbil:', error);
|
.then(async (cliModule) => {
|
||||||
|
const args = process.argv.slice(process.argv.indexOf('--cli') + 1);
|
||||||
|
const handler = new cliModule.LightweightCliHandler();
|
||||||
|
try {
|
||||||
|
await handler.handleCliMode(args);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('CLI mode error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to load CLI module:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
import('./gui').then((guiModule) => {
|
||||||
|
const app = new guiModule.GerbilApp();
|
||||||
|
app.initialize().catch((error: unknown) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to initialize Gerbil:', error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import type { LogManager } from '@/main/managers/LogManager';
|
import type { LogManager } from '@/main/managers/LogManager';
|
||||||
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||||
|
import type { OpenWebUIManager } from '@/main/managers/OpenWebUIManager';
|
||||||
import type { WindowManager } from '@/main/managers/WindowManager';
|
import type { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { HardwareManager } from '@/main/managers/HardwareManager';
|
import { HardwareManager } from '@/main/managers/HardwareManager';
|
||||||
import { BinaryManager } from '@/main/managers/BinaryManager';
|
import { BinaryManager } from '@/main/managers/BinaryManager';
|
||||||
|
|
@ -13,6 +14,7 @@ export class IPCHandlers {
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private logManager: LogManager;
|
private logManager: LogManager;
|
||||||
private sillyTavernManager: SillyTavernManager;
|
private sillyTavernManager: SillyTavernManager;
|
||||||
|
private openWebUIManager: OpenWebUIManager;
|
||||||
private hardwareManager: HardwareManager;
|
private hardwareManager: HardwareManager;
|
||||||
private binaryManager: BinaryManager;
|
private binaryManager: BinaryManager;
|
||||||
private windowManager: WindowManager;
|
private windowManager: WindowManager;
|
||||||
|
|
@ -24,12 +26,14 @@ export class IPCHandlers {
|
||||||
binaryManager: BinaryManager,
|
binaryManager: BinaryManager,
|
||||||
logManager: LogManager,
|
logManager: LogManager,
|
||||||
sillyTavernManager: SillyTavernManager,
|
sillyTavernManager: SillyTavernManager,
|
||||||
|
openWebUIManager: OpenWebUIManager,
|
||||||
windowManager: WindowManager
|
windowManager: WindowManager
|
||||||
) {
|
) {
|
||||||
this.koboldManager = koboldManager;
|
this.koboldManager = koboldManager;
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
this.logManager = logManager;
|
this.logManager = logManager;
|
||||||
this.sillyTavernManager = sillyTavernManager;
|
this.sillyTavernManager = sillyTavernManager;
|
||||||
|
this.openWebUIManager = openWebUIManager;
|
||||||
this.hardwareManager = hardwareManager;
|
this.hardwareManager = hardwareManager;
|
||||||
this.binaryManager = binaryManager;
|
this.binaryManager = binaryManager;
|
||||||
this.windowManager = windowManager;
|
this.windowManager = windowManager;
|
||||||
|
|
@ -44,6 +48,8 @@ export class IPCHandlers {
|
||||||
|
|
||||||
if (frontendPreference === 'sillytavern') {
|
if (frontendPreference === 'sillytavern') {
|
||||||
this.sillyTavernManager.startFrontend(args);
|
this.sillyTavernManager.startFrontend(args);
|
||||||
|
} else if (frontendPreference === 'openwebui') {
|
||||||
|
this.openWebUIManager.startFrontend(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -133,6 +139,7 @@ export class IPCHandlers {
|
||||||
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
||||||
this.koboldManager.stopKoboldCpp();
|
this.koboldManager.stopKoboldCpp();
|
||||||
this.sillyTavernManager.stopFrontend();
|
this.sillyTavernManager.stopFrontend();
|
||||||
|
this.openWebUIManager.stopFrontend();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
|
ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
|
||||||
|
|
@ -240,5 +247,9 @@ export class IPCHandlers {
|
||||||
ipcMain.handle('sillytavern:isNpxAvailable', () =>
|
ipcMain.handle('sillytavern:isNpxAvailable', () =>
|
||||||
this.sillyTavernManager.isNpxAvailable()
|
this.sillyTavernManager.isNpxAvailable()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('openwebui:isUvAvailable', () =>
|
||||||
|
this.openWebUIManager.isUvAvailable()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { LogManager } from '@/main/managers/LogManager';
|
import { LogManager } from '@/main/managers/LogManager';
|
||||||
import { readJsonFile, writeJsonFile } from '@/utils/fs';
|
import { readJsonFile, writeJsonFile } from '@/utils/fs';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
|
||||||
type ConfigValue = string | number | boolean | unknown[] | undefined;
|
type ConfigValue = string | number | boolean | unknown[] | undefined;
|
||||||
|
|
||||||
|
|
@ -53,8 +56,22 @@ export class ConfigManager {
|
||||||
await this.saveConfig();
|
await this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstallDir(): string | undefined {
|
private getDefaultInstallDir(): string {
|
||||||
return this.config.installDir;
|
const platform = process.platform;
|
||||||
|
const home = homedir();
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
return join(home, PRODUCT_NAME);
|
||||||
|
case 'darwin':
|
||||||
|
return join(home, 'Applications', PRODUCT_NAME);
|
||||||
|
default:
|
||||||
|
return join(home, '.local', 'share', PRODUCT_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstallDir(): string {
|
||||||
|
return this.config.installDir || this.getDefaultInstallDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInstallDir(dir: string): Promise<void> {
|
async setInstallDir(dir: string): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,6 @@ export class KoboldCppManager {
|
||||||
this.windowManager = windowManager;
|
this.windowManager = windowManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get installDir(): string {
|
|
||||||
return this.configManager.getInstallDir() || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removeDirectoryWithRetry(
|
private async removeDirectoryWithRetry(
|
||||||
dirPath: string,
|
dirPath: string,
|
||||||
maxRetries = 3,
|
maxRetries = 3,
|
||||||
|
|
@ -193,12 +189,18 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadRelease(asset: GitHubAsset): Promise<string> {
|
async downloadRelease(asset: GitHubAsset): Promise<string> {
|
||||||
const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`);
|
const tempPackedFilePath = join(
|
||||||
|
this.configManager.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}-${asset.version}`
|
||||||
: baseFilename;
|
: baseFilename;
|
||||||
const unpackedDirPath = join(this.installDir, folderName);
|
const unpackedDirPath = join(
|
||||||
|
this.configManager.getInstallDir(),
|
||||||
|
folderName
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.handleExistingDirectory(
|
await this.handleExistingDirectory(
|
||||||
|
|
@ -268,15 +270,16 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
||||||
try {
|
try {
|
||||||
if (!(await pathExists(this.installDir))) {
|
const installDir = this.configManager.getInstallDir();
|
||||||
|
if (!(await pathExists(installDir))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = await readdir(this.installDir);
|
const items = await readdir(installDir);
|
||||||
const launchers: { path: string; filename: string; size: number }[] = [];
|
const launchers: { path: string; filename: string; size: number }[] = [];
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const itemPath = join(this.installDir, item);
|
const itemPath = join(installDir, item);
|
||||||
const stats = await stat(itemPath);
|
const stats = await stat(itemPath);
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
|
|
@ -334,11 +337,12 @@ export class KoboldCppManager {
|
||||||
const configFiles: { name: string; path: string; size: number }[] = [];
|
const configFiles: { name: string; path: string; size: number }[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await pathExists(this.installDir)) {
|
const installDir = this.configManager.getInstallDir();
|
||||||
const files = await readdir(this.installDir);
|
if (await pathExists(installDir)) {
|
||||||
|
const files = await readdir(installDir);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = join(this.installDir, file);
|
const filePath = join(installDir, file);
|
||||||
|
|
||||||
const stats = await stat(filePath);
|
const stats = await stat(filePath);
|
||||||
if (
|
if (
|
||||||
|
|
@ -384,12 +388,8 @@ export class KoboldCppManager {
|
||||||
configData: KoboldConfig
|
configData: KoboldConfig
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
if (!this.installDir) {
|
const installDir = this.configManager.getInstallDir();
|
||||||
this.logManager.logError('No install directory found');
|
const configPath = join(installDir, configFileName);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configPath = join(this.installDir, configFileName);
|
|
||||||
await writeJsonFile(configPath, configData);
|
await writeJsonFile(configPath, configData);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -532,7 +532,7 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentInstallDir() {
|
getCurrentInstallDir() {
|
||||||
return this.installDir;
|
return this.configManager.getInstallDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
getWindowManager() {
|
getWindowManager() {
|
||||||
|
|
@ -543,7 +543,7 @@ export class KoboldCppManager {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
title: `Select the ${PRODUCT_NAME} Installation Directory`,
|
title: `Select the ${PRODUCT_NAME} Installation Directory`,
|
||||||
defaultPath: this.installDir,
|
defaultPath: this.configManager.getInstallDir(),
|
||||||
buttonLabel: 'Select Directory',
|
buttonLabel: 'Select Directory',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
235
src/main/managers/OpenWebUIManager.ts
Normal file
235
src/main/managers/OpenWebUIManager.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import type { ChildProcess } from 'child_process';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { LogManager } from './LogManager';
|
||||||
|
import { WindowManager } from './WindowManager';
|
||||||
|
import { ConfigManager } from './ConfigManager';
|
||||||
|
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
||||||
|
import { terminateProcess } from '@/utils/process';
|
||||||
|
import { parseKoboldConfig } from '@/utils/kobold';
|
||||||
|
|
||||||
|
export interface OpenWebUIConfig {
|
||||||
|
name: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OpenWebUIManager {
|
||||||
|
private openWebUIProcess: ChildProcess | null = null;
|
||||||
|
private logManager: LogManager;
|
||||||
|
private windowManager: WindowManager;
|
||||||
|
private configManager: ConfigManager;
|
||||||
|
private static readonly OPENWEBUI_BASE_ARGS = [
|
||||||
|
'--python',
|
||||||
|
'3.11',
|
||||||
|
'open-webui@latest',
|
||||||
|
'serve',
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
configManager: ConfigManager,
|
||||||
|
logManager: LogManager,
|
||||||
|
windowManager: WindowManager
|
||||||
|
) {
|
||||||
|
this.configManager = configManager;
|
||||||
|
this.logManager = logManager;
|
||||||
|
this.windowManager = windowManager;
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
this.cleanup().catch(() => {
|
||||||
|
void 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
this.cleanup().catch(() => {
|
||||||
|
void 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async isUvAvailable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const testProcess = spawn('uv', ['--version'], { stdio: 'pipe' });
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
testProcess.kill();
|
||||||
|
resolve(false);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
testProcess.on('exit', (code) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(code === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testProcess.on('error', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createUvProcess(
|
||||||
|
args: string[],
|
||||||
|
env?: Record<string, string>
|
||||||
|
): ChildProcess {
|
||||||
|
return spawn('uvx', args, {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
detached: false,
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForOpenWebUIToStart(): Promise<void> {
|
||||||
|
this.windowManager.sendKoboldOutput('Waiting for Open WebUI to start...');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const checkForOutput = (data: Buffer) => {
|
||||||
|
const output = data.toString();
|
||||||
|
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
|
||||||
|
this.windowManager.sendKoboldOutput('Open WebUI is now running!');
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
if (this.openWebUIProcess?.stdout) {
|
||||||
|
this.openWebUIProcess.stdout.removeListener('data', checkForOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.openWebUIProcess?.stdout) {
|
||||||
|
this.openWebUIProcess.stdout.on('data', checkForOutput);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Open WebUI process stdout not available'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startFrontend(args: string[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
name: 'openwebui',
|
||||||
|
port: OPENWEBUI.PORT,
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
host: koboldHost,
|
||||||
|
port: koboldPort,
|
||||||
|
isImageMode,
|
||||||
|
} = parseKoboldConfig(args);
|
||||||
|
|
||||||
|
if (isImageMode) {
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
'Open WebUI does not support image generation mode. Please use KoboldAI Lite or SillyTavern for image generation.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stopFrontend();
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Preparing Open WebUI to connect to KoboldCpp at ${koboldHost}:${koboldPort}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Starting ${config.name} frontend on port ${config.port}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||||
|
|
||||||
|
const openWebUIArgs = [
|
||||||
|
...OpenWebUIManager.OPENWEBUI_BASE_ARGS,
|
||||||
|
'--port',
|
||||||
|
config.port.toString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput('Starting Open WebUI with uv...');
|
||||||
|
|
||||||
|
const installDir = this.configManager.getInstallDir();
|
||||||
|
const openWebUIDataDir = join(installDir, 'openwebui-data');
|
||||||
|
|
||||||
|
this.openWebUIProcess = this.createUvProcess(openWebUIArgs, {
|
||||||
|
OPENAI_API_BASE_URL: `${koboldUrl}/v1`,
|
||||||
|
OPENAI_API_KEY: 'kobold',
|
||||||
|
DATA_DIR: openWebUIDataDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.openWebUIProcess.stdout) {
|
||||||
|
this.openWebUIProcess.stdout.on('data', (data: Buffer) => {
|
||||||
|
this.windowManager.sendKoboldOutput(data.toString(), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.openWebUIProcess.stderr) {
|
||||||
|
this.openWebUIProcess.stderr.on('data', (data: Buffer) => {
|
||||||
|
this.windowManager.sendKoboldOutput(data.toString(), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openWebUIProcess.on(
|
||||||
|
'exit',
|
||||||
|
(code: number | null, signal: string | null) => {
|
||||||
|
const message = signal
|
||||||
|
? `Open WebUI terminated with signal ${signal}`
|
||||||
|
: `Open WebUI exited with code ${code}`;
|
||||||
|
this.windowManager.sendKoboldOutput(message);
|
||||||
|
this.openWebUIProcess = null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.openWebUIProcess.on('error', (error) => {
|
||||||
|
this.logManager.logError('Open WebUI process error:', error);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Open WebUI error: ${error.message}`
|
||||||
|
);
|
||||||
|
this.openWebUIProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.waitForOpenWebUIToStart();
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Open WebUI is ready and auto-configured for KoboldCpp!`
|
||||||
|
);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Access Open WebUI at: http://localhost:${config.port}`
|
||||||
|
);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`KoboldCpp connection: ${koboldUrl}/v1 (auto-configured)`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError(
|
||||||
|
'Failed to start Open WebUI frontend:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Failed to start Open WebUI: ${(error as Error).message}`
|
||||||
|
);
|
||||||
|
this.openWebUIProcess = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopFrontend(): Promise<void> {
|
||||||
|
if (this.openWebUIProcess) {
|
||||||
|
this.windowManager.sendKoboldOutput('Stopping Open WebUI...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await terminateProcess(this.openWebUIProcess, {
|
||||||
|
logError: (message, error) =>
|
||||||
|
this.logManager.logError(message, error),
|
||||||
|
});
|
||||||
|
this.windowManager.sendKoboldOutput('Open WebUI stopped');
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError('Error stopping Open WebUI:', error as Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openWebUIProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
await this.stopFrontend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,10 @@ import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
import { LogManager } from './LogManager';
|
import { LogManager } from './LogManager';
|
||||||
import { WindowManager } from './WindowManager';
|
import { WindowManager } from './WindowManager';
|
||||||
import { SILLYTAVERN } from '@/constants';
|
import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { terminateProcess } from '@/utils/process';
|
import { terminateProcess } from '@/utils/process';
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/fs';
|
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/fs';
|
||||||
|
import { parseKoboldConfig } from '@/utils/kobold';
|
||||||
|
|
||||||
export interface SillyTavernConfig {
|
export interface SillyTavernConfig {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -76,39 +77,6 @@ export class SillyTavernManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallback = this.getFallbackDataRoot();
|
const fallback = this.getFallbackDataRoot();
|
||||||
|
|
||||||
if (await pathExists(fallback)) {
|
|
||||||
this.detectedDataRoot = fallback;
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = process.platform;
|
|
||||||
const home = homedir();
|
|
||||||
const alternatePaths = [];
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case 'win32':
|
|
||||||
alternatePaths.push(
|
|
||||||
join(home, 'AppData', 'Local', 'SillyTavern', 'data'),
|
|
||||||
join(home, '.sillytavern', 'data')
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'darwin':
|
|
||||||
alternatePaths.push(join(home, '.sillytavern', 'data'));
|
|
||||||
break;
|
|
||||||
case 'linux':
|
|
||||||
default:
|
|
||||||
alternatePaths.push(join(home, '.sillytavern', 'data'));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const path of alternatePaths) {
|
|
||||||
if (await pathExists(path)) {
|
|
||||||
this.detectedDataRoot = path;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.detectedDataRoot = fallback;
|
this.detectedDataRoot = fallback;
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +188,7 @@ export class SillyTavernManager {
|
||||||
initProcess.stdout.on('data', (data: Buffer) => {
|
initProcess.stdout.on('data', (data: Buffer) => {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
|
|
||||||
if (output.includes('SillyTavern is listening')) {
|
if (output.includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (!initProcess.killed && !hasResolved) {
|
if (!initProcess.killed && !hasResolved) {
|
||||||
hasResolved = true;
|
hasResolved = true;
|
||||||
|
|
@ -249,33 +217,6 @@ export class SillyTavernManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseKoboldConfig(args: string[]): {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
isImageMode: boolean;
|
|
||||||
} {
|
|
||||||
let host = 'localhost';
|
|
||||||
let port = 5001;
|
|
||||||
let hasSdModel = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < args.length - 1; i++) {
|
|
||||||
if (args[i] === '--hostname' || args[i] === '--host') {
|
|
||||||
host = args[i + 1];
|
|
||||||
} else if (args[i] === '--port') {
|
|
||||||
const parsedPort = parseInt(args[i + 1], 10);
|
|
||||||
if (!isNaN(parsedPort)) {
|
|
||||||
port = parsedPort;
|
|
||||||
}
|
|
||||||
} else if (args[i] === '--sdmodel') {
|
|
||||||
hasSdModel = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isImageMode = hasSdModel;
|
|
||||||
|
|
||||||
return { host, port, isImageMode };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setupSillyTavernConfig(
|
private async setupSillyTavernConfig(
|
||||||
koboldHost: string,
|
koboldHost: string,
|
||||||
koboldPort: number,
|
koboldPort: number,
|
||||||
|
|
@ -357,13 +298,8 @@ export class SillyTavernManager {
|
||||||
this.windowManager.sendKoboldOutput('Waiting for SillyTavern to start...');
|
this.windowManager.sendKoboldOutput('Waiting for SillyTavern to start...');
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reject(new Error('SillyTavern failed to start within 30 seconds'));
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
const checkForOutput = (data: Buffer) => {
|
const checkForOutput = (data: Buffer) => {
|
||||||
if (data.toString().includes('SillyTavern is listening')) {
|
if (data.toString().includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
||||||
clearTimeout(timeout);
|
|
||||||
this.windowManager.sendKoboldOutput('SillyTavern is now running!');
|
this.windowManager.sendKoboldOutput('SillyTavern is now running!');
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
|
|
@ -379,7 +315,6 @@ export class SillyTavernManager {
|
||||||
if (this.sillyTavernProcess?.stdout) {
|
if (this.sillyTavernProcess?.stdout) {
|
||||||
this.sillyTavernProcess.stdout.on('data', checkForOutput);
|
this.sillyTavernProcess.stdout.on('data', checkForOutput);
|
||||||
} else {
|
} else {
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(new Error('SillyTavern process stdout not available'));
|
reject(new Error('SillyTavern process stdout not available'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -433,7 +368,7 @@ export class SillyTavernManager {
|
||||||
host: koboldHost,
|
host: koboldHost,
|
||||||
port: koboldPort,
|
port: koboldPort,
|
||||||
isImageMode,
|
isImageMode,
|
||||||
} = this.parseKoboldConfig(args);
|
} = parseKoboldConfig(args);
|
||||||
|
|
||||||
await this.stopFrontend();
|
await this.stopFrontend();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
ConfigAPI,
|
ConfigAPI,
|
||||||
LogsAPI,
|
LogsAPI,
|
||||||
SillyTavernAPI,
|
SillyTavernAPI,
|
||||||
|
OpenWebUIAPI,
|
||||||
} from '@/types/electron';
|
} from '@/types/electron';
|
||||||
|
|
||||||
const koboldAPI: KoboldAPI = {
|
const koboldAPI: KoboldAPI = {
|
||||||
|
|
@ -100,10 +101,15 @@ const sillyTavernAPI: SillyTavernAPI = {
|
||||||
isNpxAvailable: () => ipcRenderer.invoke('sillytavern:isNpxAvailable'),
|
isNpxAvailable: () => ipcRenderer.invoke('sillytavern:isNpxAvailable'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openwebUIAPI: OpenWebUIAPI = {
|
||||||
|
isUvAvailable: () => ipcRenderer.invoke('openwebui:isUvAvailable'),
|
||||||
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
kobold: koboldAPI,
|
kobold: koboldAPI,
|
||||||
app: appAPI,
|
app: appAPI,
|
||||||
config: configAPI,
|
config: configAPI,
|
||||||
logs: logsAPI,
|
logs: logsAPI,
|
||||||
sillytavern: sillyTavernAPI,
|
sillytavern: sillyTavernAPI,
|
||||||
|
openwebui: openwebUIAPI,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
|
|
@ -166,6 +166,10 @@ export interface SillyTavernAPI {
|
||||||
isNpxAvailable: () => Promise<boolean>;
|
isNpxAvailable: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenWebUIAPI {
|
||||||
|
isUvAvailable: () => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
|
|
@ -174,6 +178,7 @@ declare global {
|
||||||
config: ConfigAPI;
|
config: ConfigAPI;
|
||||||
logs: LogsAPI;
|
logs: LogsAPI;
|
||||||
sillytavern: SillyTavernAPI;
|
sillytavern: SillyTavernAPI;
|
||||||
|
openwebui: OpenWebUIAPI;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
|
|
@ -8,7 +8,7 @@ export type InterfaceTab = 'terminal' | 'chat';
|
||||||
|
|
||||||
export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
|
export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
|
||||||
|
|
||||||
export type FrontendPreference = 'koboldcpp' | 'sillytavern';
|
export type FrontendPreference = 'koboldcpp' | 'sillytavern' | 'openwebui';
|
||||||
|
|
||||||
export type Screen = 'welcome' | 'download' | 'launch' | 'interface';
|
export type Screen = 'welcome' | 'download' | 'launch' | 'interface';
|
||||||
|
|
||||||
|
|
|
||||||
28
src/utils/kobold.ts
Normal file
28
src/utils/kobold.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
export interface KoboldConfig {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
isImageMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseKoboldConfig(args: string[]): KoboldConfig {
|
||||||
|
let host = 'localhost';
|
||||||
|
let port = 5001;
|
||||||
|
let hasSdModel = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length - 1; i++) {
|
||||||
|
if (args[i] === '--hostname' || args[i] === '--host') {
|
||||||
|
host = args[i + 1];
|
||||||
|
} else if (args[i] === '--port') {
|
||||||
|
const parsedPort = parseInt(args[i + 1], 10);
|
||||||
|
if (!isNaN(parsedPort)) {
|
||||||
|
port = parsedPort;
|
||||||
|
}
|
||||||
|
} else if (args[i] === '--sdmodel') {
|
||||||
|
hasSdModel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isImageMode = hasSdModel;
|
||||||
|
|
||||||
|
return { host, port, isImageMode };
|
||||||
|
}
|
||||||
142
yarn.lock
142
yarn.lock
|
|
@ -1358,106 +1358,106 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@npm:^8.41.0":
|
"@typescript-eslint/eslint-plugin@npm:^8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.41.0"
|
resolution: "@typescript-eslint/eslint-plugin@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||||
"@typescript-eslint/scope-manager": "npm:8.41.0"
|
"@typescript-eslint/scope-manager": "npm:8.42.0"
|
||||||
"@typescript-eslint/type-utils": "npm:8.41.0"
|
"@typescript-eslint/type-utils": "npm:8.42.0"
|
||||||
"@typescript-eslint/utils": "npm:8.41.0"
|
"@typescript-eslint/utils": "npm:8.42.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||||
graphemer: "npm:^1.4.0"
|
graphemer: "npm:^1.4.0"
|
||||||
ignore: "npm:^7.0.0"
|
ignore: "npm:^7.0.0"
|
||||||
natural-compare: "npm:^1.4.0"
|
natural-compare: "npm:^1.4.0"
|
||||||
ts-api-utils: "npm:^2.1.0"
|
ts-api-utils: "npm:^2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@typescript-eslint/parser": ^8.41.0
|
"@typescript-eslint/parser": ^8.42.0
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/29812ee5deeae65e67db29faa8d96bc70255c45788f342b11838850ea29a96e4331622cad3e703ffacaa895372845d44fd6b04786117c78f1a027595adff2e62
|
checksum: 10c0/835fd7497f0e4eaef55dc3d94079acc0ad1dc74735916915f160419b1e7f44d04fbce683b4871148d1af33046bd5ae3fed59103d4c49460776b560c42173bbff
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/parser@npm:^8.41.0":
|
"@typescript-eslint/parser@npm:^8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/parser@npm:8.41.0"
|
resolution: "@typescript-eslint/parser@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/scope-manager": "npm:8.41.0"
|
"@typescript-eslint/scope-manager": "npm:8.42.0"
|
||||||
"@typescript-eslint/types": "npm:8.41.0"
|
"@typescript-eslint/types": "npm:8.42.0"
|
||||||
"@typescript-eslint/typescript-estree": "npm:8.41.0"
|
"@typescript-eslint/typescript-estree": "npm:8.42.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/ca13ff505e9253aee761741f96714cd65a296bbfcac961efbbf7a909ff3d180b2142a23db0a2a5e50b928fa56586528b7e47ba6301089dd850945018dbf2ef50
|
checksum: 10c0/f071154bce7f874449236919a7367d977317959fe6d454fe5369ca54dee7d057fe3b8b250c5990ea4205a9c52fd59702da63d1721895c72d745168aa31532112
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/project-service@npm:8.41.0":
|
"@typescript-eslint/project-service@npm:8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/project-service@npm:8.41.0"
|
resolution: "@typescript-eslint/project-service@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/tsconfig-utils": "npm:^8.41.0"
|
"@typescript-eslint/tsconfig-utils": "npm:^8.42.0"
|
||||||
"@typescript-eslint/types": "npm:^8.41.0"
|
"@typescript-eslint/types": "npm:^8.42.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/907ba880fcaf0805fc97012b431536b5b06db6ae4a0095708f9d9a4406feddabd964f09ea4ca99d8fa7bd141dbcc9496f1a9eb6683361a6bb01fb714a361126c
|
checksum: 10c0/788b0bc52683be376cd768a4fed3202cdaccc86f231ec94a0f6bbb1389fdfd0e14c505f03015cefb73869de63c8089b78a169ed957048a1e5ee1b6250ec19604
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@npm:8.41.0":
|
"@typescript-eslint/scope-manager@npm:8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/scope-manager@npm:8.41.0"
|
resolution: "@typescript-eslint/scope-manager@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types": "npm:8.41.0"
|
"@typescript-eslint/types": "npm:8.42.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||||
checksum: 10c0/6b339ac1fc37a1e05dc6de421db9f9b138c357497ec87af2471ad30e48c78b4979d3da40943a1c81fc85d1537326a4f938843434db63d29eff414b9364daf8e8
|
checksum: 10c0/caca15f2124909c588ed3e48fe0769ad8baa296a0b229f724ec94f5f746e486e08dd49eeddd66d01f09e2ddaed03f9e18d7b535a44196d413f283e22f929f623
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@npm:8.41.0, @typescript-eslint/tsconfig-utils@npm:^8.41.0":
|
"@typescript-eslint/tsconfig-utils@npm:8.42.0, @typescript-eslint/tsconfig-utils@npm:^8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.41.0"
|
resolution: "@typescript-eslint/tsconfig-utils@npm:8.42.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/98618a536b9cb071eacba2970ce2ca1b9243de78f4604c2e350823a5275b9d7d15238dbe6acd197c30c0b6cbbf37782c247d14984e1015a109431e4180d76af6
|
checksum: 10c0/03882eeee279fafa2cb4ee3154742417fd29395b3bfe3f867d9d4cb9cb68d1200c885c35b96dd558a1aff8561ac3700cff8ca7680a5cf34e5e0e136a6ee3c30c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@npm:8.41.0":
|
"@typescript-eslint/type-utils@npm:8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/type-utils@npm:8.41.0"
|
resolution: "@typescript-eslint/type-utils@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types": "npm:8.41.0"
|
"@typescript-eslint/types": "npm:8.42.0"
|
||||||
"@typescript-eslint/typescript-estree": "npm:8.41.0"
|
"@typescript-eslint/typescript-estree": "npm:8.42.0"
|
||||||
"@typescript-eslint/utils": "npm:8.41.0"
|
"@typescript-eslint/utils": "npm:8.42.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
ts-api-utils: "npm:^2.1.0"
|
ts-api-utils: "npm:^2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/d4f9ae07a30f1cf331c3e3a67f8749b38f199ba5000f7a600492c27f6bec774f15c3553f293c520fb999fb88108665f2785d5261daec1445b17af14a7bb0bfac
|
checksum: 10c0/47e5f7276cafd7719d3e2f2e456fa988927e658d15c2c188a692d9c639f9d76f582a6c133cb1bf01eba9027e1022eb6b79b57861a96302460e5e847c2b536afa
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/types@npm:8.41.0, @typescript-eslint/types@npm:^8.41.0":
|
"@typescript-eslint/types@npm:8.42.0, @typescript-eslint/types@npm:^8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/types@npm:8.41.0"
|
resolution: "@typescript-eslint/types@npm:8.42.0"
|
||||||
checksum: 10c0/4945a7ed7789e0527833ee378b962416d6d0d61eb6c891fe49cb6c8dc8a9adbfc58676080ca767a1f034f74f9a981caf5f4d4706cba5025c0520a801fb45d7e1
|
checksum: 10c0/d585dff5005328282cc59f9402e886a3db64727906ad3e68b49d7ef73bc07bef3ed569287ba826ebaa07b69be42a72232a38529951d64c28cebd83db0892cd33
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@npm:8.41.0":
|
"@typescript-eslint/typescript-estree@npm:8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/typescript-estree@npm:8.41.0"
|
resolution: "@typescript-eslint/typescript-estree@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/project-service": "npm:8.41.0"
|
"@typescript-eslint/project-service": "npm:8.42.0"
|
||||||
"@typescript-eslint/tsconfig-utils": "npm:8.41.0"
|
"@typescript-eslint/tsconfig-utils": "npm:8.42.0"
|
||||||
"@typescript-eslint/types": "npm:8.41.0"
|
"@typescript-eslint/types": "npm:8.42.0"
|
||||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
fast-glob: "npm:^3.3.2"
|
fast-glob: "npm:^3.3.2"
|
||||||
is-glob: "npm:^4.0.3"
|
is-glob: "npm:^4.0.3"
|
||||||
|
|
@ -1466,32 +1466,32 @@ __metadata:
|
||||||
ts-api-utils: "npm:^2.1.0"
|
ts-api-utils: "npm:^2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/e86233d895403ec4986ced25f56898b2704a84545bb7dfe933f5c64f2ab969dcb7ada7e21ea7e015c875cc94a0767e70573442724960c631b7b3fc556a984c9c
|
checksum: 10c0/2d3354d780421cfa90f812048984c43cd47aabecef7a5c0f56ad0b91331cb369d1c8366da90bf9a8f6df47df3741f9e16897e998f16270ac55376f519b775c23
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/utils@npm:8.41.0":
|
"@typescript-eslint/utils@npm:8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/utils@npm:8.41.0"
|
resolution: "@typescript-eslint/utils@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
||||||
"@typescript-eslint/scope-manager": "npm:8.41.0"
|
"@typescript-eslint/scope-manager": "npm:8.42.0"
|
||||||
"@typescript-eslint/types": "npm:8.41.0"
|
"@typescript-eslint/types": "npm:8.42.0"
|
||||||
"@typescript-eslint/typescript-estree": "npm:8.41.0"
|
"@typescript-eslint/typescript-estree": "npm:8.42.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: ">=4.8.4 <6.0.0"
|
typescript: ">=4.8.4 <6.0.0"
|
||||||
checksum: 10c0/3a2ed9b5f801afeccde44dbacdeae0b9c82cc3e1af5e92926929ad86384dc0fb0027152e68c5edfabe904647c2160c0c45ec9c848a8d67c3efb86b78a1343acb
|
checksum: 10c0/acf30019023669ddae00c02cabfa74fc12defccd4703e552ab5115edbeceaaf1688c1586873bf66aefeb3f03eb1ed456905403303913c724db38bf030e40a700
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@npm:8.41.0":
|
"@typescript-eslint/visitor-keys@npm:8.42.0":
|
||||||
version: 8.41.0
|
version: 8.42.0
|
||||||
resolution: "@typescript-eslint/visitor-keys@npm:8.41.0"
|
resolution: "@typescript-eslint/visitor-keys@npm:8.42.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types": "npm:8.41.0"
|
"@typescript-eslint/types": "npm:8.42.0"
|
||||||
eslint-visitor-keys: "npm:^4.2.1"
|
eslint-visitor-keys: "npm:^4.2.1"
|
||||||
checksum: 10c0/cfe52e77b9e07c23a4d9f4adf9e6bf27822e58694c9a34fefa4b9fc96d553e9df561971c4da5fc78392522e34696fc1149a76f6a02c328136771c5efe0fd1029
|
checksum: 10c0/22c942f2a100d71c08f952b976446e824ddf227d4ac02b7016e12d4a33804ab06072d570695baed3565d0a08a1d3fa6ff3ccf97a122d63e65780f871d597f1b1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -2747,16 +2747,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"electron@npm:^37.4.0":
|
"electron@npm:^38.0.0":
|
||||||
version: 37.4.0
|
version: 38.0.0
|
||||||
resolution: "electron@npm:37.4.0"
|
resolution: "electron@npm:38.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get": "npm:^2.0.0"
|
"@electron/get": "npm:^2.0.0"
|
||||||
"@types/node": "npm:^22.7.7"
|
"@types/node": "npm:^22.7.7"
|
||||||
extract-zip: "npm:^2.0.1"
|
extract-zip: "npm:^2.0.1"
|
||||||
bin:
|
bin:
|
||||||
electron: cli.js
|
electron: cli.js
|
||||||
checksum: 10c0/92a0c41190e234d302bc612af6cce9af08cd07f6699c1ff21a9365297e73dc9d88c6c4c25ddabf352447e3e555878d2ab0f2f31a14e210dda6de74d2787ff323
|
checksum: 10c0/df3e4eaead2b0de4612a1b593f46667dfd4aff4755727371b5630aada40094cddf82b3837ce0ba3babed8c503275da59187f11da9f4ab26cd20e1362dc103ea6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -3711,12 +3711,12 @@ __metadata:
|
||||||
"@types/node": "npm:^24.3.0"
|
"@types/node": "npm:^24.3.0"
|
||||||
"@types/react": "npm:^19.1.12"
|
"@types/react": "npm:^19.1.12"
|
||||||
"@types/react-dom": "npm:^19.1.9"
|
"@types/react-dom": "npm:^19.1.9"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^8.41.0"
|
"@typescript-eslint/eslint-plugin": "npm:^8.42.0"
|
||||||
"@typescript-eslint/parser": "npm:^8.41.0"
|
"@typescript-eslint/parser": "npm:^8.42.0"
|
||||||
"@vitejs/plugin-react": "npm:^5.0.2"
|
"@vitejs/plugin-react": "npm:^5.0.2"
|
||||||
axios: "npm:^1.11.0"
|
axios: "npm:^1.11.0"
|
||||||
cross-env: "npm:^10.0.0"
|
cross-env: "npm:^10.0.0"
|
||||||
electron: "npm:^37.4.0"
|
electron: "npm:^38.0.0"
|
||||||
electron-builder: "npm:^26.0.12"
|
electron-builder: "npm:^26.0.12"
|
||||||
electron-vite: "npm:^4.0.0"
|
electron-vite: "npm:^4.0.0"
|
||||||
eslint: "npm:^9.34.0"
|
eslint: "npm:^9.34.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue