mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -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
|
||||
flux1-kontext-dev-Q3_K_S.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.
|
||||
|
||||
<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 -->
|
||||
|
||||
## 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)
|
||||
- **Automatic updates** - Download and keep your KoboldCpp binary up-to-date effortlessly
|
||||
- **Smart process management** - Prevents runaway background processes and system resource waste
|
||||
- **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
|
||||
- **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
|
||||
|
||||
## Installation
|
||||
|
|
@ -49,9 +50,6 @@ The AUR package automatically handles installation, desktop integration, and sys
|
|||
|
||||
## Screenshots
|
||||
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<div align="center">
|
||||
|
||||
### Download & Setup
|
||||
|
||||
<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">
|
||||
|
||||
### Seemless SillyTavern integration
|
||||
### Seamless SillyTavern integration
|
||||
|
||||
<img src="screenshots/sillytavern.png" alt="SillyTavern integration" width="600">
|
||||
|
||||
</div>
|
||||
<!-- markdownlint-enable MD033 -->
|
||||
### Seamless OpenWebUI integration
|
||||
|
||||
<img src="screenshots/openwebui.png" alt="SillyTavern integration" width="600">
|
||||
|
||||
### 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
|
||||
|
||||
AGPL v3 License - see LICENSE file for details
|
||||
|
||||
# test
|
||||
|
|
|
|||
17
package.json
17
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gerbil",
|
||||
"productName": "Gerbil",
|
||||
"version": "0.9.6",
|
||||
"version": "0.9.7",
|
||||
"description": "Run Large Language Models locally",
|
||||
"main": "out/main/index.js",
|
||||
"homepage": "./",
|
||||
|
|
@ -56,11 +56,11 @@
|
|||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||
"@typescript-eslint/parser": "^8.41.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"cross-env": "^10.0.0",
|
||||
"electron": "^37.4.0",
|
||||
"electron": "^38.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^4.0.0",
|
||||
"eslint": "^9.34.0",
|
||||
|
|
@ -116,15 +116,6 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "src/main/templates",
|
||||
"to": "templates",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"compression": "maximum",
|
||||
"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={[
|
||||
{
|
||||
value: 'chat',
|
||||
label:
|
||||
frontendPreference === 'sillytavern'
|
||||
? FRONTENDS.SILLYTAVERN
|
||||
: isImageGenerationMode
|
||||
? FRONTENDS.STABLE_UI
|
||||
: FRONTENDS.KOBOLDAI_LITE,
|
||||
label: (() => {
|
||||
if (frontendPreference === 'sillytavern') {
|
||||
return FRONTENDS.SILLYTAVERN;
|
||||
}
|
||||
if (
|
||||
frontendPreference === 'openwebui' &&
|
||||
!isImageGenerationMode
|
||||
) {
|
||||
return FRONTENDS.OPENWEBUI;
|
||||
}
|
||||
return isImageGenerationMode
|
||||
? FRONTENDS.STABLE_UI
|
||||
: FRONTENDS.KOBOLDAI_LITE;
|
||||
})(),
|
||||
},
|
||||
{ value: 'terminal', label: 'Terminal' },
|
||||
{ value: 'eject', label: 'Eject' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
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 type { FrontendPreference } from '@/types';
|
||||
|
||||
|
|
@ -46,6 +51,9 @@ export const ServerTab = ({
|
|||
if (frontendPreference === 'sillytavern') {
|
||||
iframeUrl = SILLYTAVERN.PROXY_URL;
|
||||
title = FRONTENDS.SILLYTAVERN;
|
||||
} else if (frontendPreference === 'openwebui' && !isImageGenerationMode) {
|
||||
iframeUrl = OPENWEBUI.URL;
|
||||
title = FRONTENDS.OPENWEBUI;
|
||||
} else {
|
||||
iframeUrl = isImageGenerationMode ? `${serverUrl}/sdui` : serverUrl;
|
||||
title = isImageGenerationMode
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const TerminalTab = ({
|
|||
onServerReady,
|
||||
frontendPreference = 'koboldcpp',
|
||||
}: TerminalTabProps) => {
|
||||
const { host, port } = useLaunchConfigStore();
|
||||
const { host, port, isImageGenerationMode } = useLaunchConfigStore();
|
||||
const computedColorScheme = useComputedColorScheme('light', {
|
||||
getInitialValueInEffect: false,
|
||||
});
|
||||
|
|
@ -68,20 +68,22 @@ export const TerminalTab = ({
|
|||
const serverHost = host || 'localhost';
|
||||
const serverPort = port || 5001;
|
||||
|
||||
let signalToCheck: string = SERVER_READY_SIGNALS.KOBOLDCPP;
|
||||
|
||||
if (frontendPreference === 'sillytavern') {
|
||||
if (newData.includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
||||
setTimeout(
|
||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||
1500
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (newData.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
|
||||
setTimeout(
|
||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||
1500
|
||||
);
|
||||
}
|
||||
signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN;
|
||||
} else if (
|
||||
frontendPreference === 'openwebui' &&
|
||||
!isImageGenerationMode
|
||||
) {
|
||||
signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI;
|
||||
}
|
||||
|
||||
if (newData.includes(signalToCheck)) {
|
||||
setTimeout(
|
||||
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||
1500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +92,7 @@ export const TerminalTab = ({
|
|||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onServerReady, host, port, frontendPreference]);
|
||||
}, [onServerReady, host, port, frontendPreference, isImageGenerationMode]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
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 {
|
||||
Stack,
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
Button,
|
||||
rem,
|
||||
Select,
|
||||
Box,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import { Folder, FolderOpen, Monitor } from 'lucide-react';
|
||||
|
|
@ -15,43 +16,125 @@ import styles from '@/styles/layout.module.css';
|
|||
import type { FrontendPreference } from '@/types';
|
||||
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 = () => {
|
||||
const [installDir, setInstallDir] = useState<string>('');
|
||||
const [FrontendPreference, setFrontendPreference] =
|
||||
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(() => {
|
||||
const initialize = async () => {
|
||||
await Promise.all([loadCurrentInstallDir(), loadFrontendPreference()]);
|
||||
await Promise.all([
|
||||
loadCurrentInstallDir(),
|
||||
loadFrontendPreference(),
|
||||
checkAllFrontendRequirements(),
|
||||
]);
|
||||
};
|
||||
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(() => {
|
||||
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) {
|
||||
checkNpxAvailability();
|
||||
checkAllFrontendRequirements();
|
||||
}
|
||||
}, [FrontendPreference]);
|
||||
}, [FrontendPreference, checkAllFrontendRequirements]);
|
||||
|
||||
const loadCurrentInstallDir = async () => {
|
||||
const currentDir = await safeExecute(
|
||||
|
|
@ -86,14 +169,15 @@ export const GeneralTab = () => {
|
|||
};
|
||||
|
||||
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(
|
||||
() => window.electronAPI.config.set('frontendPreference', value),
|
||||
'Failed to save frontend preference:'
|
||||
);
|
||||
if (success) {
|
||||
setFrontendPreference(value);
|
||||
setFrontendPreference(value as FrontendPreference);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -130,43 +214,76 @@ export const GeneralTab = () => {
|
|||
<Text fw={500} mb="sm">
|
||||
Frontend Interface
|
||||
</Text>
|
||||
{isNpxAvailable && (
|
||||
|
||||
{getUnmetRequirements().length === 0 && (
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Choose which frontend interface to use for interacting with AI
|
||||
models
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isNpxAvailable && (
|
||||
{getUnmetRequirements().length > 0 && (
|
||||
<Text size="sm" c="red" mb="md">
|
||||
Custom frontends require{' '}
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.electronAPI.app.openExternal('https://nodejs.org/');
|
||||
}}
|
||||
c="red"
|
||||
td="underline"
|
||||
>
|
||||
Node.js
|
||||
</Anchor>{' '}
|
||||
{getSelectedFrontendConfig()?.label} requires{' '}
|
||||
{getUnmetRequirements().map((req, index) => (
|
||||
<span key={req.id}>
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.electronAPI.app.openExternal(req.url);
|
||||
}}
|
||||
c="red"
|
||||
td="underline"
|
||||
>
|
||||
{req.name}
|
||||
</Anchor>
|
||||
{index < getUnmetRequirements().length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
))}{' '}
|
||||
to be installed on your system
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Select
|
||||
value={FrontendPreference}
|
||||
onChange={handleFrontendPreferenceChange}
|
||||
disabled={!isNpxAvailable}
|
||||
data={[
|
||||
{ value: 'koboldcpp', label: 'KoboldCpp (Built-in)' },
|
||||
{
|
||||
value: 'sillytavern',
|
||||
label: FRONTENDS.SILLYTAVERN,
|
||||
},
|
||||
]}
|
||||
data={frontendConfigs.map((config) => ({
|
||||
value: config.value,
|
||||
label: config.label,
|
||||
disabled: !isFrontendAvailable(config.value),
|
||||
}))}
|
||||
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>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,3 +13,10 @@ export const SILLYTAVERN = {
|
|||
return `http://localhost:${this.PROXY_PORT}`;
|
||||
},
|
||||
} 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 = {
|
||||
KOBOLDCPP: 'Please connect to custom endpoint at',
|
||||
SILLYTAVERN: 'SillyTavern is listening on',
|
||||
OPENWEBUI: 'Waiting for application startup.',
|
||||
} as const;
|
||||
|
||||
export * from './defaults';
|
||||
|
|
@ -45,6 +46,7 @@ export const FRONTENDS = {
|
|||
KOBOLDAI_LITE: 'KoboldAI Lite',
|
||||
STABLE_UI: 'Stable UI',
|
||||
SILLYTAVERN: 'SillyTavern',
|
||||
OPENWEBUI: 'Open WebUI',
|
||||
} as const;
|
||||
|
||||
export const ZOOM = {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { LogManager } from '@/main/managers/LogManager';
|
||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||
import { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||
import { OpenWebUIManager } from '@/main/managers/OpenWebUIManager';
|
||||
import { HardwareManager } from '@/main/managers/HardwareManager';
|
||||
import { BinaryManager } from '@/main/managers/BinaryManager';
|
||||
import { IPCHandlers } from '@/main/ipc';
|
||||
import { PRODUCT_NAME } from '@/constants';
|
||||
import { homedir } from 'os';
|
||||
import { ensureDir } from '@/utils/fs';
|
||||
import { getConfigDir } from '@/utils/path';
|
||||
|
||||
|
|
@ -20,6 +18,7 @@ export class GerbilApp {
|
|||
private configManager: ConfigManager;
|
||||
private logManager: LogManager;
|
||||
private sillyTavernManager: SillyTavernManager;
|
||||
private openWebUIManager: OpenWebUIManager;
|
||||
private hardwareManager: HardwareManager;
|
||||
private binaryManager: BinaryManager;
|
||||
private ipcHandlers: IPCHandlers;
|
||||
|
|
@ -49,6 +48,12 @@ export class GerbilApp {
|
|||
this.windowManager
|
||||
);
|
||||
|
||||
this.openWebUIManager = new OpenWebUIManager(
|
||||
this.configManager,
|
||||
this.logManager,
|
||||
this.windowManager
|
||||
);
|
||||
|
||||
this.ipcHandlers = new IPCHandlers(
|
||||
this.koboldManager,
|
||||
this.configManager,
|
||||
|
|
@ -56,32 +61,13 @@ export class GerbilApp {
|
|||
this.binaryManager,
|
||||
this.logManager,
|
||||
this.sillyTavernManager,
|
||||
this.openWebUIManager,
|
||||
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> {
|
||||
const installDir =
|
||||
this.configManager.getInstallDir() || this.getDefaultInstallDir();
|
||||
|
||||
if (!this.configManager.getInstallDir()) {
|
||||
await this.configManager.setInstallDir(installDir);
|
||||
}
|
||||
|
||||
const installDir = this.configManager.getInstallDir();
|
||||
await ensureDir(installDir);
|
||||
}
|
||||
|
||||
|
|
@ -90,10 +76,6 @@ export class GerbilApp {
|
|||
await this.configManager.initialize();
|
||||
await this.ensureInstallDirectory();
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.setAppUserModelId('com.gerbil.app');
|
||||
}
|
||||
|
||||
this.windowManager.createMainWindow();
|
||||
this.ipcHandlers.setupHandlers();
|
||||
|
||||
|
|
@ -112,6 +94,7 @@ export class GerbilApp {
|
|||
const cleanupPromises = [
|
||||
this.koboldManager.cleanup(),
|
||||
this.sillyTavernManager.cleanup(),
|
||||
this.openWebUIManager.cleanup(),
|
||||
];
|
||||
|
||||
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) {
|
||||
import('./cli')
|
||||
.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) => {
|
||||
if (process.argv[1] === '--version') {
|
||||
(async () => {
|
||||
try {
|
||||
const packageJsonPath = join(__dirname, '../../package.json');
|
||||
const packageJson = await readJsonFile<{ version: string }>(
|
||||
packageJsonPath
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to load CLI module:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
console.log(packageJson?.version || 'unknown');
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('unknown');
|
||||
}
|
||||
process.exit(0);
|
||||
})();
|
||||
} 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);
|
||||
const isCliMode = process.argv.includes('--cli');
|
||||
|
||||
if (isCliMode) {
|
||||
import('./cli')
|
||||
.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 { LogManager } from '@/main/managers/LogManager';
|
||||
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||
import type { OpenWebUIManager } from '@/main/managers/OpenWebUIManager';
|
||||
import type { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { HardwareManager } from '@/main/managers/HardwareManager';
|
||||
import { BinaryManager } from '@/main/managers/BinaryManager';
|
||||
|
|
@ -13,6 +14,7 @@ export class IPCHandlers {
|
|||
private configManager: ConfigManager;
|
||||
private logManager: LogManager;
|
||||
private sillyTavernManager: SillyTavernManager;
|
||||
private openWebUIManager: OpenWebUIManager;
|
||||
private hardwareManager: HardwareManager;
|
||||
private binaryManager: BinaryManager;
|
||||
private windowManager: WindowManager;
|
||||
|
|
@ -24,12 +26,14 @@ export class IPCHandlers {
|
|||
binaryManager: BinaryManager,
|
||||
logManager: LogManager,
|
||||
sillyTavernManager: SillyTavernManager,
|
||||
openWebUIManager: OpenWebUIManager,
|
||||
windowManager: WindowManager
|
||||
) {
|
||||
this.koboldManager = koboldManager;
|
||||
this.configManager = configManager;
|
||||
this.logManager = logManager;
|
||||
this.sillyTavernManager = sillyTavernManager;
|
||||
this.openWebUIManager = openWebUIManager;
|
||||
this.hardwareManager = hardwareManager;
|
||||
this.binaryManager = binaryManager;
|
||||
this.windowManager = windowManager;
|
||||
|
|
@ -44,6 +48,8 @@ export class IPCHandlers {
|
|||
|
||||
if (frontendPreference === 'sillytavern') {
|
||||
this.sillyTavernManager.startFrontend(args);
|
||||
} else if (frontendPreference === 'openwebui') {
|
||||
this.openWebUIManager.startFrontend(args);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -133,6 +139,7 @@ export class IPCHandlers {
|
|||
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
||||
this.koboldManager.stopKoboldCpp();
|
||||
this.sillyTavernManager.stopFrontend();
|
||||
this.openWebUIManager.stopFrontend();
|
||||
});
|
||||
|
||||
ipcMain.handle('kobold:parseConfigFile', (_, filePath) =>
|
||||
|
|
@ -240,5 +247,9 @@ export class IPCHandlers {
|
|||
ipcMain.handle('sillytavern:isNpxAvailable', () =>
|
||||
this.sillyTavernManager.isNpxAvailable()
|
||||
);
|
||||
|
||||
ipcMain.handle('openwebui:isUvAvailable', () =>
|
||||
this.openWebUIManager.isUvAvailable()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { LogManager } from '@/main/managers/LogManager';
|
||||
import { readJsonFile, writeJsonFile } from '@/utils/fs';
|
||||
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;
|
||||
|
||||
|
|
@ -53,8 +56,22 @@ export class ConfigManager {
|
|||
await this.saveConfig();
|
||||
}
|
||||
|
||||
getInstallDir(): string | undefined {
|
||||
return this.config.installDir;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
getInstallDir(): string {
|
||||
return this.config.installDir || this.getDefaultInstallDir();
|
||||
}
|
||||
|
||||
async setInstallDir(dir: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@ export class KoboldCppManager {
|
|||
this.windowManager = windowManager;
|
||||
}
|
||||
|
||||
private get installDir(): string {
|
||||
return this.configManager.getInstallDir() || '';
|
||||
}
|
||||
|
||||
private async removeDirectoryWithRetry(
|
||||
dirPath: string,
|
||||
maxRetries = 3,
|
||||
|
|
@ -193,12 +189,18 @@ export class KoboldCppManager {
|
|||
}
|
||||
|
||||
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 folderName = asset.version
|
||||
? `${baseFilename}-${asset.version}`
|
||||
: baseFilename;
|
||||
const unpackedDirPath = join(this.installDir, folderName);
|
||||
const unpackedDirPath = join(
|
||||
this.configManager.getInstallDir(),
|
||||
folderName
|
||||
);
|
||||
|
||||
try {
|
||||
await this.handleExistingDirectory(
|
||||
|
|
@ -268,15 +270,16 @@ export class KoboldCppManager {
|
|||
|
||||
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
||||
try {
|
||||
if (!(await pathExists(this.installDir))) {
|
||||
const installDir = this.configManager.getInstallDir();
|
||||
if (!(await pathExists(installDir))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = await readdir(this.installDir);
|
||||
const items = await readdir(installDir);
|
||||
const launchers: { path: string; filename: string; size: number }[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = join(this.installDir, item);
|
||||
const itemPath = join(installDir, item);
|
||||
const stats = await stat(itemPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
|
|
@ -334,11 +337,12 @@ export class KoboldCppManager {
|
|||
const configFiles: { name: string; path: string; size: number }[] = [];
|
||||
|
||||
try {
|
||||
if (await pathExists(this.installDir)) {
|
||||
const files = await readdir(this.installDir);
|
||||
const installDir = this.configManager.getInstallDir();
|
||||
if (await pathExists(installDir)) {
|
||||
const files = await readdir(installDir);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(this.installDir, file);
|
||||
const filePath = join(installDir, file);
|
||||
|
||||
const stats = await stat(filePath);
|
||||
if (
|
||||
|
|
@ -384,12 +388,8 @@ export class KoboldCppManager {
|
|||
configData: KoboldConfig
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!this.installDir) {
|
||||
this.logManager.logError('No install directory found');
|
||||
return false;
|
||||
}
|
||||
|
||||
const configPath = join(this.installDir, configFileName);
|
||||
const installDir = this.configManager.getInstallDir();
|
||||
const configPath = join(installDir, configFileName);
|
||||
await writeJsonFile(configPath, configData);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -532,7 +532,7 @@ export class KoboldCppManager {
|
|||
}
|
||||
|
||||
getCurrentInstallDir() {
|
||||
return this.installDir;
|
||||
return this.configManager.getInstallDir();
|
||||
}
|
||||
|
||||
getWindowManager() {
|
||||
|
|
@ -543,7 +543,7 @@ export class KoboldCppManager {
|
|||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
title: `Select the ${PRODUCT_NAME} Installation Directory`,
|
||||
defaultPath: this.installDir,
|
||||
defaultPath: this.configManager.getInstallDir(),
|
||||
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 { WindowManager } from './WindowManager';
|
||||
import { SILLYTAVERN } from '@/constants';
|
||||
import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { terminateProcess } from '@/utils/process';
|
||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/fs';
|
||||
import { parseKoboldConfig } from '@/utils/kobold';
|
||||
|
||||
export interface SillyTavernConfig {
|
||||
name: string;
|
||||
|
|
@ -76,39 +77,6 @@ export class SillyTavernManager {
|
|||
}
|
||||
|
||||
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;
|
||||
return fallback;
|
||||
}
|
||||
|
|
@ -220,7 +188,7 @@ export class SillyTavernManager {
|
|||
initProcess.stdout.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
|
||||
if (output.includes('SillyTavern is listening')) {
|
||||
if (output.includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
||||
setTimeout(async () => {
|
||||
if (!initProcess.killed && !hasResolved) {
|
||||
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(
|
||||
koboldHost: string,
|
||||
koboldPort: number,
|
||||
|
|
@ -357,13 +298,8 @@ export class SillyTavernManager {
|
|||
this.windowManager.sendKoboldOutput('Waiting for SillyTavern to start...');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('SillyTavern failed to start within 30 seconds'));
|
||||
}, 30000);
|
||||
|
||||
const checkForOutput = (data: Buffer) => {
|
||||
if (data.toString().includes('SillyTavern is listening')) {
|
||||
clearTimeout(timeout);
|
||||
if (data.toString().includes(SERVER_READY_SIGNALS.SILLYTAVERN)) {
|
||||
this.windowManager.sendKoboldOutput('SillyTavern is now running!');
|
||||
resolve();
|
||||
|
||||
|
|
@ -379,7 +315,6 @@ export class SillyTavernManager {
|
|||
if (this.sillyTavernProcess?.stdout) {
|
||||
this.sillyTavernProcess.stdout.on('data', checkForOutput);
|
||||
} else {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error('SillyTavern process stdout not available'));
|
||||
}
|
||||
});
|
||||
|
|
@ -433,7 +368,7 @@ export class SillyTavernManager {
|
|||
host: koboldHost,
|
||||
port: koboldPort,
|
||||
isImageMode,
|
||||
} = this.parseKoboldConfig(args);
|
||||
} = parseKoboldConfig(args);
|
||||
|
||||
await this.stopFrontend();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
ConfigAPI,
|
||||
LogsAPI,
|
||||
SillyTavernAPI,
|
||||
OpenWebUIAPI,
|
||||
} from '@/types/electron';
|
||||
|
||||
const koboldAPI: KoboldAPI = {
|
||||
|
|
@ -100,10 +101,15 @@ const sillyTavernAPI: SillyTavernAPI = {
|
|||
isNpxAvailable: () => ipcRenderer.invoke('sillytavern:isNpxAvailable'),
|
||||
};
|
||||
|
||||
const openwebUIAPI: OpenWebUIAPI = {
|
||||
isUvAvailable: () => ipcRenderer.invoke('openwebui:isUvAvailable'),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
kobold: koboldAPI,
|
||||
app: appAPI,
|
||||
config: configAPI,
|
||||
logs: logsAPI,
|
||||
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>;
|
||||
}
|
||||
|
||||
export interface OpenWebUIAPI {
|
||||
isUvAvailable: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
|
|
@ -174,6 +178,7 @@ declare global {
|
|||
config: ConfigAPI;
|
||||
logs: LogsAPI;
|
||||
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 FrontendPreference = 'koboldcpp' | 'sillytavern';
|
||||
export type FrontendPreference = 'koboldcpp' | 'sillytavern' | 'openwebui';
|
||||
|
||||
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
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:^8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.41.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:^8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.42.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.41.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.41.0"
|
||||
"@typescript-eslint/utils": "npm:8.41.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.42.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.42.0"
|
||||
"@typescript-eslint/utils": "npm:8.42.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||
graphemer: "npm:^1.4.0"
|
||||
ignore: "npm:^7.0.0"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.41.0
|
||||
"@typescript-eslint/parser": ^8.42.0
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/29812ee5deeae65e67db29faa8d96bc70255c45788f342b11838850ea29a96e4331622cad3e703ffacaa895372845d44fd6b04786117c78f1a027595adff2e62
|
||||
checksum: 10c0/835fd7497f0e4eaef55dc3d94079acc0ad1dc74735916915f160419b1e7f44d04fbce683b4871148d1af33046bd5ae3fed59103d4c49460776b560c42173bbff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:^8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.41.0"
|
||||
"@typescript-eslint/parser@npm:^8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.42.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.41.0"
|
||||
"@typescript-eslint/types": "npm:8.41.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.41.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.42.0"
|
||||
"@typescript-eslint/types": "npm:8.42.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.42.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/ca13ff505e9253aee761741f96714cd65a296bbfcac961efbbf7a909ff3d180b2142a23db0a2a5e50b928fa56586528b7e47ba6301089dd850945018dbf2ef50
|
||||
checksum: 10c0/f071154bce7f874449236919a7367d977317959fe6d454fe5369ca54dee7d057fe3b8b250c5990ea4205a9c52fd59702da63d1721895c72d745168aa31532112
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.41.0"
|
||||
"@typescript-eslint/project-service@npm:8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.42.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.41.0"
|
||||
"@typescript-eslint/types": "npm:^8.41.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.42.0"
|
||||
"@typescript-eslint/types": "npm:^8.42.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/907ba880fcaf0805fc97012b431536b5b06db6ae4a0095708f9d9a4406feddabd964f09ea4ca99d8fa7bd141dbcc9496f1a9eb6683361a6bb01fb714a361126c
|
||||
checksum: 10c0/788b0bc52683be376cd768a4fed3202cdaccc86f231ec94a0f6bbb1389fdfd0e14c505f03015cefb73869de63c8089b78a169ed957048a1e5ee1b6250ec19604
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.41.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.42.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.41.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
||||
checksum: 10c0/6b339ac1fc37a1e05dc6de421db9f9b138c357497ec87af2471ad30e48c78b4979d3da40943a1c81fc85d1537326a4f938843434db63d29eff414b9364daf8e8
|
||||
"@typescript-eslint/types": "npm:8.42.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||
checksum: 10c0/caca15f2124909c588ed3e48fe0769ad8baa296a0b229f724ec94f5f746e486e08dd49eeddd66d01f09e2ddaed03f9e18d7b535a44196d413f283e22f929f623
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.41.0, @typescript-eslint/tsconfig-utils@npm:^8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@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.42.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.42.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/98618a536b9cb071eacba2970ce2ca1b9243de78f4604c2e350823a5275b9d7d15238dbe6acd197c30c0b6cbbf37782c247d14984e1015a109431e4180d76af6
|
||||
checksum: 10c0/03882eeee279fafa2cb4ee3154742417fd29395b3bfe3f867d9d4cb9cb68d1200c885c35b96dd558a1aff8561ac3700cff8ca7680a5cf34e5e0e136a6ee3c30c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.41.0"
|
||||
"@typescript-eslint/type-utils@npm:8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.42.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.41.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.41.0"
|
||||
"@typescript-eslint/utils": "npm:8.41.0"
|
||||
"@typescript-eslint/types": "npm:8.42.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.42.0"
|
||||
"@typescript-eslint/utils": "npm:8.42.0"
|
||||
debug: "npm:^4.3.4"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/d4f9ae07a30f1cf331c3e3a67f8749b38f199ba5000f7a600492c27f6bec774f15c3553f293c520fb999fb88108665f2785d5261daec1445b17af14a7bb0bfac
|
||||
checksum: 10c0/47e5f7276cafd7719d3e2f2e456fa988927e658d15c2c188a692d9c639f9d76f582a6c133cb1bf01eba9027e1022eb6b79b57861a96302460e5e847c2b536afa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.41.0, @typescript-eslint/types@npm:^8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/types@npm:8.41.0"
|
||||
checksum: 10c0/4945a7ed7789e0527833ee378b962416d6d0d61eb6c891fe49cb6c8dc8a9adbfc58676080ca767a1f034f74f9a981caf5f4d4706cba5025c0520a801fb45d7e1
|
||||
"@typescript-eslint/types@npm:8.42.0, @typescript-eslint/types@npm:^8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/types@npm:8.42.0"
|
||||
checksum: 10c0/d585dff5005328282cc59f9402e886a3db64727906ad3e68b49d7ef73bc07bef3ed569287ba826ebaa07b69be42a72232a38529951d64c28cebd83db0892cd33
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.41.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.42.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.41.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.41.0"
|
||||
"@typescript-eslint/types": "npm:8.41.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.41.0"
|
||||
"@typescript-eslint/project-service": "npm:8.42.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.42.0"
|
||||
"@typescript-eslint/types": "npm:8.42.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.42.0"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
|
|
@ -1466,32 +1466,32 @@ __metadata:
|
|||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/e86233d895403ec4986ced25f56898b2704a84545bb7dfe933f5c64f2ab969dcb7ada7e21ea7e015c875cc94a0767e70573442724960c631b7b3fc556a984c9c
|
||||
checksum: 10c0/2d3354d780421cfa90f812048984c43cd47aabecef7a5c0f56ad0b91331cb369d1c8366da90bf9a8f6df47df3741f9e16897e998f16270ac55376f519b775c23
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.41.0"
|
||||
"@typescript-eslint/utils@npm:8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.42.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.41.0"
|
||||
"@typescript-eslint/types": "npm:8.41.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.41.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.42.0"
|
||||
"@typescript-eslint/types": "npm:8.42.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.42.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10c0/3a2ed9b5f801afeccde44dbacdeae0b9c82cc3e1af5e92926929ad86384dc0fb0027152e68c5edfabe904647c2160c0c45ec9c848a8d67c3efb86b78a1343acb
|
||||
checksum: 10c0/acf30019023669ddae00c02cabfa74fc12defccd4703e552ab5115edbeceaaf1688c1586873bf66aefeb3f03eb1ed456905403303913c724db38bf030e40a700
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.41.0":
|
||||
version: 8.41.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.41.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.42.0":
|
||||
version: 8.42.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.42.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.41.0"
|
||||
"@typescript-eslint/types": "npm:8.42.0"
|
||||
eslint-visitor-keys: "npm:^4.2.1"
|
||||
checksum: 10c0/cfe52e77b9e07c23a4d9f4adf9e6bf27822e58694c9a34fefa4b9fc96d553e9df561971c4da5fc78392522e34696fc1149a76f6a02c328136771c5efe0fd1029
|
||||
checksum: 10c0/22c942f2a100d71c08f952b976446e824ddf227d4ac02b7016e12d4a33804ab06072d570695baed3565d0a08a1d3fa6ff3ccf97a122d63e65780f871d597f1b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -2747,16 +2747,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron@npm:^37.4.0":
|
||||
version: 37.4.0
|
||||
resolution: "electron@npm:37.4.0"
|
||||
"electron@npm:^38.0.0":
|
||||
version: 38.0.0
|
||||
resolution: "electron@npm:38.0.0"
|
||||
dependencies:
|
||||
"@electron/get": "npm:^2.0.0"
|
||||
"@types/node": "npm:^22.7.7"
|
||||
extract-zip: "npm:^2.0.1"
|
||||
bin:
|
||||
electron: cli.js
|
||||
checksum: 10c0/92a0c41190e234d302bc612af6cce9af08cd07f6699c1ff21a9365297e73dc9d88c6c4c25ddabf352447e3e555878d2ab0f2f31a14e210dda6de74d2787ff323
|
||||
checksum: 10c0/df3e4eaead2b0de4612a1b593f46667dfd4aff4755727371b5630aada40094cddf82b3837ce0ba3babed8c503275da59187f11da9f4ab26cd20e1362dc103ea6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3711,12 +3711,12 @@ __metadata:
|
|||
"@types/node": "npm:^24.3.0"
|
||||
"@types/react": "npm:^19.1.12"
|
||||
"@types/react-dom": "npm:^19.1.9"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.41.0"
|
||||
"@typescript-eslint/parser": "npm:^8.41.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.42.0"
|
||||
"@typescript-eslint/parser": "npm:^8.42.0"
|
||||
"@vitejs/plugin-react": "npm:^5.0.2"
|
||||
axios: "npm:^1.11.0"
|
||||
cross-env: "npm:^10.0.0"
|
||||
electron: "npm:^37.4.0"
|
||||
electron: "npm:^38.0.0"
|
||||
electron-builder: "npm:^26.0.12"
|
||||
electron-vite: "npm:^4.0.0"
|
||||
eslint: "npm:^9.34.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue