mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
adding seamless integration for sillytavern as an optional frontend, refactors to reduce code duplication, add http link detection to terminal output
This commit is contained in:
parent
861a1afb87
commit
a559f8c86d
45 changed files with 2173 additions and 685 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,3 +16,4 @@ ae.safetensors
|
||||||
clip_l.safetensors
|
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
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -90,28 +90,34 @@ The `--cli` argument allows you to use the Friendly Kobold binary as a proxy to
|
||||||
|
|
||||||
### Considerations
|
### Considerations
|
||||||
|
|
||||||
You might want to run CLI Mode if you're looking to use a different frontend, such as SillyTavern or OpenWebUI, than the ones bundled (eg. KoboldAI Lite, Stable UI) with KoboldCpp AND you're looking to minimize any resource utilization of this app. Note that at the time of this writing, Friendly Kobold only takes about ~200MB of RAM and ~100MB of VRAM for its Chromium-based UI. When running in CLI Mode, Friendly Kobold will still take about 1/3 of those RAM and VRAM numbers.
|
You might want to run CLI Mode if you're looking to use a different frontend, such as OpenWebUI, than the ones bundled (eg. KoboldAI Lite, Stable UI) with KoboldCpp AND you're looking to minimize any resource utilization of this app. Note that at the time of this writing, Friendly Kobold only takes about ~200MB of RAM and ~100MB of VRAM for its Chromium-based UI. When running in CLI Mode, Friendly Kobold will still take about 1/3 of those RAM and VRAM numbers.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Linux/macOS:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic usage - launch KoboldCpp with no arguments
|
# Basic usage - launch KoboldCpp launcher with no arguments
|
||||||
./friendly-kobold --cli
|
friendly-kobold --cli
|
||||||
|
|
||||||
# Pass arguments to KoboldCpp
|
# Pass arguments to KoboldCpp
|
||||||
./friendly-kobold --cli --help
|
friendly-kobold --cli --help
|
||||||
./friendly-kobold --cli --port 5001 --model /path/to/model.gguf
|
friendly-kobold --cli --port 5001 --model /path/to/model.gguf
|
||||||
|
|
||||||
# Any KoboldCpp arguments are supported
|
# Any KoboldCpp arguments are supported
|
||||||
./friendly-kobold --cli --model /path/to/model.gguf --port 5001 --host 0.0.0.0 --multiuser 2
|
friendly-kobold --cli --model /path/to/model.gguf --port 5001 --host 0.0.0.0 --multiuser 2
|
||||||
|
|
||||||
|
# CLI inception (friendly kobold CLI calling KoboldCpp CLI mode)
|
||||||
|
# This is the ideal way to run a custom frontend
|
||||||
|
friendly-kobold --cli --cli --model /path/to/model.gguf --gpulayers 57 --contextsize 8192 --port 5001 --multiuser 1 --flashattention --usemmap --usevulkan
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows:**
|
**Windows:**
|
||||||
|
|
||||||
CLI mode will only work correctly on Windows if you install Friendly Kobold using the Setup.exe from the github releases. Otherwise there is currently a technical limitation with the Windows portable .exe which will cause it to not display the terminal output correctly nor will it be killable through the standard terminal (Ctrl+C) commands.
|
CLI mode will only work correctly on Windows if you install Friendly Kobold using the Setup.exe from the github releases. Otherwise there is currently a technical limitation with the Windows portable .exe which will cause it to not display the terminal output correctly nor will it be killable through the standard terminal (Ctrl+C) commands.
|
||||||
|
|
||||||
|
You can use the CLI mode on Windows in exactly the same way as in the Linux/macOS examples above, except you'll be calling the "Friendly Kobold.exe". Note that it will not be on your system PATH by default, so you'll need to manually specify the full path to it when callig it from the Windows terminal.
|
||||||
|
|
||||||
## For Local Dev
|
## For Local Dev
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
@ -139,10 +145,6 @@ CLI mode will only work correctly on Windows if you install Friendly Kobold usin
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Future considerations
|
|
||||||
|
|
||||||
It would make a lot of sense to transition this project to Tauri from Electron. The app size should drop from ~110MB to ~10MB; however, users on obsolete OSes (with outdated WebViews) will very likely encounter issues. In addition, I would need to learn Rust to rewrite the BE (Electron main code), but at least we can re-use all the React code. The app would be much smaller, faster and memory efficient, but not work for some users. I think it's a worthy tradeoff.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL v3 License - see LICENSE file for details
|
AGPL v3 License - see LICENSE file for details
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ const config = [
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
...react.configs.recommended.rules,
|
...react.configs.recommended.rules,
|
||||||
|
...importPlugin.configs.recommended.rules,
|
||||||
|
...importPlugin.configs.typescript.rules,
|
||||||
|
...tseslint.configs.recommended.rules,
|
||||||
|
...tseslint.configs.stylistic.rules,
|
||||||
|
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
|
|
@ -102,6 +106,11 @@ const config = [
|
||||||
|
|
||||||
'import/no-default-export': 'error',
|
'import/no-default-export': 'error',
|
||||||
'import/prefer-default-export': 'off',
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/no-relative-parent-imports': 'error',
|
||||||
|
|
||||||
|
'@typescript-eslint/array-type': ['error', { default: 'array' }],
|
||||||
|
'@typescript-eslint/no-require-imports': 'error',
|
||||||
|
|
||||||
'arrow-body-style': ['error', 'as-needed'],
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "friendly-kobold",
|
"name": "friendly-kobold",
|
||||||
"productName": "Friendly Kobold",
|
"productName": "Friendly Kobold",
|
||||||
"version": "0.8.1",
|
"version": "0.9.0",
|
||||||
"description": "A desktop app for running Large Language Models locally",
|
"description": "A desktop app for running Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
@ -54,14 +54,13 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.34.0",
|
"@eslint/js": "^9.34.0",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.8",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@types/strip-ansi": "^5.2.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
"@typescript-eslint/parser": "^8.41.0",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
"@vitejs/plugin-react": "^5.0.1",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"electron": "^37.3.1",
|
"electron": "^37.4.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",
|
||||||
|
|
@ -86,9 +85,9 @@
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"got": "^14.4.7",
|
"got": "^14.4.7",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
|
"npm": "^11.5.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"strip-ansi": "^7.1.0",
|
|
||||||
"systeminformation": "^5.27.8",
|
"systeminformation": "^5.27.8",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
|
|
|
||||||
226
src/App.tsx
226
src/App.tsx
|
|
@ -13,9 +13,7 @@ import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
||||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||||
import { UI } from '@/constants';
|
import { UI } from '@/constants';
|
||||||
import type { DownloadItem } from '@/types/electron';
|
import type { DownloadItem } from '@/types/electron';
|
||||||
import type { InterfaceTab } from '@/types';
|
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
||||||
|
|
||||||
type Screen = 'welcome' | 'download' | 'launch' | 'interface';
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
||||||
|
|
@ -25,6 +23,8 @@ export const App = () => {
|
||||||
const [activeInterfaceTab, setActiveInterfaceTab] =
|
const [activeInterfaceTab, setActiveInterfaceTab] =
|
||||||
useState<InterfaceTab>('terminal');
|
useState<InterfaceTab>('terminal');
|
||||||
const [isImageGenerationMode, setIsImageGenerationMode] = useState(false);
|
const [isImageGenerationMode, setIsImageGenerationMode] = useState(false);
|
||||||
|
const [frontendPreference, setFrontendPreference] =
|
||||||
|
useState<FrontendPreference>('koboldcpp');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
updateInfo: binaryUpdateInfo,
|
updateInfo: binaryUpdateInfo,
|
||||||
|
|
@ -46,15 +46,19 @@ export const App = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkInstallation = async () => {
|
const checkInstallation = async () => {
|
||||||
try {
|
try {
|
||||||
const [versions, currentBinaryPath, hasSeenWelcome] = await Promise.all(
|
const [versions, currentBinaryPath, hasSeenWelcome, preference] =
|
||||||
[
|
await Promise.all([
|
||||||
window.electronAPI.kobold.getInstalledVersions(),
|
window.electronAPI.kobold.getInstalledVersions(),
|
||||||
window.electronAPI.config.get(
|
window.electronAPI.config.get(
|
||||||
'currentKoboldBinary'
|
'currentKoboldBinary'
|
||||||
) as Promise<string>,
|
) as Promise<string>,
|
||||||
window.electronAPI.config.get('hasSeenWelcome') as Promise<boolean>,
|
window.electronAPI.config.get('hasSeenWelcome') as Promise<boolean>,
|
||||||
]
|
window.electronAPI.config.get(
|
||||||
);
|
'frontendPreference'
|
||||||
|
) as Promise<FrontendPreference>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFrontendPreference(preference || 'koboldcpp');
|
||||||
|
|
||||||
if (!hasSeenWelcome) {
|
if (!hasSeenWelcome) {
|
||||||
setCurrentScreenWithTransition('welcome');
|
setCurrentScreenWithTransition('welcome');
|
||||||
|
|
@ -214,106 +218,118 @@ export const App = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AppShell
|
||||||
<AppShell
|
header={{ height: currentScreen === 'welcome' ? 0 : UI.HEADER_HEIGHT }}
|
||||||
header={{ height: currentScreen === 'welcome' ? 0 : UI.HEADER_HEIGHT }}
|
padding={currentScreen === 'interface' ? 0 : 'md'}
|
||||||
padding={currentScreen === 'interface' ? 0 : 'md'}
|
>
|
||||||
|
{currentScreen !== 'welcome' && (
|
||||||
|
<AppHeader
|
||||||
|
currentScreen={currentScreen}
|
||||||
|
activeInterfaceTab={activeInterfaceTab}
|
||||||
|
setActiveInterfaceTab={setActiveInterfaceTab}
|
||||||
|
isImageGenerationMode={isImageGenerationMode}
|
||||||
|
frontendPreference={frontendPreference}
|
||||||
|
onEject={handleEject}
|
||||||
|
onSettingsOpen={() => setSettingsOpened(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AppShell.Main
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight:
|
||||||
|
currentScreen === 'welcome'
|
||||||
|
? '100vh'
|
||||||
|
: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{currentScreen !== 'welcome' && (
|
{currentScreen === null ? (
|
||||||
<AppHeader
|
<Center h="100%" style={{ minHeight: '25rem' }}>
|
||||||
currentScreen={currentScreen}
|
<Stack align="center" gap="lg">
|
||||||
activeInterfaceTab={activeInterfaceTab}
|
<Loader size="xl" type="dots" />
|
||||||
setActiveInterfaceTab={setActiveInterfaceTab}
|
<Text c="dimmed" size="lg">
|
||||||
isImageGenerationMode={isImageGenerationMode}
|
Loading...
|
||||||
onEject={handleEject}
|
</Text>
|
||||||
onSettingsOpen={() => setSettingsOpened(true)}
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'welcome'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<WelcomeScreen onGetStarted={handleWelcomeComplete} />
|
||||||
|
</ScreenTransition>
|
||||||
|
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'download'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<DownloadScreen onDownloadComplete={handleDownloadComplete} />
|
||||||
|
</ScreenTransition>
|
||||||
|
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'launch'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<LaunchScreen
|
||||||
|
onLaunch={handleLaunch}
|
||||||
|
onLaunchModeChange={setIsImageGenerationMode}
|
||||||
|
/>
|
||||||
|
</ScreenTransition>
|
||||||
|
|
||||||
|
<ScreenTransition
|
||||||
|
isActive={currentScreen === 'interface'}
|
||||||
|
shouldAnimate={hasInitialized}
|
||||||
|
>
|
||||||
|
<InterfaceScreen
|
||||||
|
activeTab={activeInterfaceTab}
|
||||||
|
onTabChange={setActiveInterfaceTab}
|
||||||
|
isImageGenerationMode={isImageGenerationMode}
|
||||||
|
/>
|
||||||
|
</ScreenTransition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showUpdateModal && binaryUpdateInfo && (
|
||||||
|
<UpdateAvailableModal
|
||||||
|
opened={showUpdateModal}
|
||||||
|
onClose={dismissUpdate}
|
||||||
|
currentVersion={binaryUpdateInfo.currentVersion}
|
||||||
|
availableUpdate={binaryUpdateInfo.availableUpdate}
|
||||||
|
onUpdate={handleBinaryUpdate}
|
||||||
|
isDownloading={
|
||||||
|
downloading === binaryUpdateInfo.availableUpdate.name
|
||||||
|
}
|
||||||
|
downloadProgress={
|
||||||
|
downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AppShell.Main
|
</AppShell.Main>
|
||||||
style={{
|
<SettingsModal
|
||||||
position: 'relative',
|
opened={settingsOpened}
|
||||||
overflow: 'hidden',
|
onClose={async () => {
|
||||||
minHeight:
|
setSettingsOpened(false);
|
||||||
currentScreen === 'welcome'
|
try {
|
||||||
? '100vh'
|
const preference = (await window.electronAPI.config.get(
|
||||||
: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
|
'frontendPreference'
|
||||||
}}
|
)) as FrontendPreference;
|
||||||
>
|
setFrontendPreference(preference || 'koboldcpp');
|
||||||
{currentScreen === null ? (
|
} catch (error) {
|
||||||
<Center h="100%" style={{ minHeight: '25rem' }}>
|
window.electronAPI.logs.logError(
|
||||||
<Stack align="center" gap="lg">
|
'Failed to load frontend preference:',
|
||||||
<Loader size="xl" type="dots" />
|
error as Error
|
||||||
<Text c="dimmed" size="lg">
|
);
|
||||||
Loading...
|
}
|
||||||
</Text>
|
}}
|
||||||
</Stack>
|
currentScreen={currentScreen || undefined}
|
||||||
</Center>
|
/>
|
||||||
) : (
|
<EjectConfirmModal
|
||||||
<>
|
opened={showEjectModal}
|
||||||
<ScreenTransition
|
onClose={() => setShowEjectModal(false)}
|
||||||
isActive={currentScreen === 'welcome'}
|
onConfirm={handleEjectConfirm}
|
||||||
shouldAnimate={hasInitialized}
|
/>
|
||||||
>
|
</AppShell>
|
||||||
<WelcomeScreen onGetStarted={handleWelcomeComplete} />
|
|
||||||
</ScreenTransition>
|
|
||||||
|
|
||||||
<ScreenTransition
|
|
||||||
isActive={currentScreen === 'download'}
|
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<DownloadScreen onDownloadComplete={handleDownloadComplete} />
|
|
||||||
</ScreenTransition>
|
|
||||||
|
|
||||||
<ScreenTransition
|
|
||||||
isActive={currentScreen === 'launch'}
|
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<LaunchScreen
|
|
||||||
onLaunch={handleLaunch}
|
|
||||||
onLaunchModeChange={setIsImageGenerationMode}
|
|
||||||
/>
|
|
||||||
</ScreenTransition>
|
|
||||||
|
|
||||||
<ScreenTransition
|
|
||||||
isActive={currentScreen === 'interface'}
|
|
||||||
shouldAnimate={hasInitialized}
|
|
||||||
>
|
|
||||||
<InterfaceScreen
|
|
||||||
activeTab={activeInterfaceTab}
|
|
||||||
onTabChange={setActiveInterfaceTab}
|
|
||||||
isImageGenerationMode={isImageGenerationMode}
|
|
||||||
/>
|
|
||||||
</ScreenTransition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showUpdateModal && binaryUpdateInfo && (
|
|
||||||
<UpdateAvailableModal
|
|
||||||
opened={showUpdateModal}
|
|
||||||
onClose={dismissUpdate}
|
|
||||||
currentVersion={binaryUpdateInfo.currentVersion}
|
|
||||||
availableUpdate={binaryUpdateInfo.availableUpdate}
|
|
||||||
onUpdate={handleBinaryUpdate}
|
|
||||||
isDownloading={
|
|
||||||
downloading === binaryUpdateInfo.availableUpdate.name
|
|
||||||
}
|
|
||||||
downloadProgress={
|
|
||||||
downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AppShell.Main>
|
|
||||||
<SettingsModal
|
|
||||||
opened={settingsOpened}
|
|
||||||
onClose={() => setSettingsOpened(false)}
|
|
||||||
currentScreen={currentScreen || undefined}
|
|
||||||
/>
|
|
||||||
<EjectConfirmModal
|
|
||||||
opened={showEjectModal}
|
|
||||||
onClose={() => setShowEjectModal(false)}
|
|
||||||
onConfirm={handleEjectConfirm}
|
|
||||||
/>
|
|
||||||
</AppShell>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ import {
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Settings, ArrowLeft } from 'lucide-react';
|
import { Settings, ArrowLeft } from 'lucide-react';
|
||||||
import { soundAssets, playSound, initializeAudio } from '@/utils';
|
import { soundAssets, playSound, initializeAudio } from '@/utils';
|
||||||
import type { InterfaceTab } from '@/types';
|
|
||||||
import iconUrl from '/icon.png';
|
import iconUrl from '/icon.png';
|
||||||
|
import { FRONTENDS } from '@/constants';
|
||||||
type Screen = 'welcome' | 'download' | 'launch' | 'interface';
|
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
|
||||||
|
|
||||||
interface AppHeaderProps {
|
interface AppHeaderProps {
|
||||||
currentScreen: Screen | null;
|
currentScreen: Screen | null;
|
||||||
activeInterfaceTab: InterfaceTab;
|
activeInterfaceTab: InterfaceTab;
|
||||||
setActiveInterfaceTab: (tab: InterfaceTab) => void;
|
setActiveInterfaceTab: (tab: InterfaceTab) => void;
|
||||||
isImageGenerationMode: boolean;
|
isImageGenerationMode: boolean;
|
||||||
|
frontendPreference: FrontendPreference;
|
||||||
onEject: () => void;
|
onEject: () => void;
|
||||||
onSettingsOpen: () => void;
|
onSettingsOpen: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ export const AppHeader = ({
|
||||||
activeInterfaceTab,
|
activeInterfaceTab,
|
||||||
setActiveInterfaceTab,
|
setActiveInterfaceTab,
|
||||||
isImageGenerationMode,
|
isImageGenerationMode,
|
||||||
|
frontendPreference,
|
||||||
onEject,
|
onEject,
|
||||||
onSettingsOpen,
|
onSettingsOpen,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
|
|
@ -117,7 +118,12 @@ export const AppHeader = ({
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: 'chat',
|
value: 'chat',
|
||||||
label: isImageGenerationMode ? 'Stable UI' : 'KoboldAI Lite',
|
label:
|
||||||
|
frontendPreference === 'sillytavern'
|
||||||
|
? FRONTENDS.SILLYTAVERN
|
||||||
|
: isImageGenerationMode
|
||||||
|
? FRONTENDS.STABLE_UI
|
||||||
|
: FRONTENDS.KOBOLDAI_LITE,
|
||||||
},
|
},
|
||||||
{ value: 'terminal', label: 'Terminal' },
|
{ value: 'terminal', label: 'Terminal' },
|
||||||
]}
|
]}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Box, Text, Stack } from '@mantine/core';
|
import { Box, Text, Stack } from '@mantine/core';
|
||||||
import { UI } from '@/constants';
|
import { UI, SILLYTAVERN, FRONTENDS } from '@/constants';
|
||||||
import type { ServerTabMode } from '@/types';
|
import type { ServerTabMode, FrontendPreference } from '@/types';
|
||||||
|
|
||||||
interface ServerTabProps {
|
interface ServerTabProps {
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
isServerReady?: boolean;
|
isServerReady?: boolean;
|
||||||
mode: ServerTabMode;
|
mode: ServerTabMode;
|
||||||
|
frontendPreference?: FrontendPreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServerTab = ({
|
export const ServerTab = ({
|
||||||
serverUrl,
|
serverUrl,
|
||||||
isServerReady,
|
isServerReady,
|
||||||
mode,
|
mode,
|
||||||
|
frontendPreference = 'koboldcpp',
|
||||||
}: ServerTabProps) => {
|
}: ServerTabProps) => {
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
|
|
@ -29,7 +31,7 @@ export const ServerTab = ({
|
||||||
>
|
>
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Text c="dimmed" size="lg">
|
<Text c="dimmed" size="lg">
|
||||||
Waiting for KoboldCpp server to start...
|
Waiting for the KoboldCpp server to start...
|
||||||
</Text>
|
</Text>
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
The {mode === 'chat' ? 'chat' : 'image generation'} interface will
|
The {mode === 'chat' ? 'chat' : 'image generation'} interface will
|
||||||
|
|
@ -40,12 +42,19 @@ export const ServerTab = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframeUrl =
|
let iframeUrl: string;
|
||||||
mode === 'image-generation' ? `${serverUrl}/sdui` : serverUrl;
|
let title: string;
|
||||||
const title =
|
|
||||||
mode === 'image-generation'
|
if (frontendPreference === 'sillytavern') {
|
||||||
? 'Stable UI Interface'
|
iframeUrl = SILLYTAVERN.PROXY_URL;
|
||||||
: 'KoboldAI Lite Interface';
|
title = FRONTENDS.SILLYTAVERN;
|
||||||
|
} else {
|
||||||
|
iframeUrl = mode === 'image-generation' ? `${serverUrl}/sdui` : serverUrl;
|
||||||
|
title =
|
||||||
|
mode === 'image-generation'
|
||||||
|
? FRONTENDS.STABLE_UI
|
||||||
|
: FRONTENDS.KOBOLDAI_LITE;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,15 @@ import {
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import styles from '@/styles/layout.module.css';
|
import styles from '@/styles/layout.module.css';
|
||||||
import { UI } from '@/constants';
|
import { UI } from '@/constants';
|
||||||
import { handleTerminalOutput } from '@/utils';
|
import { handleTerminalOutput, processTerminalContent } from '@/utils';
|
||||||
|
import { useLaunchConfigStore } from '@/stores/launchConfigStore';
|
||||||
|
|
||||||
interface TerminalTabProps {
|
interface TerminalTabProps {
|
||||||
onServerReady?: (serverUrl: string) => void;
|
onServerReady: (url: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
||||||
|
const { host, port } = useLaunchConfigStore();
|
||||||
const computedColorScheme = useComputedColorScheme('light', {
|
const computedColorScheme = useComputedColorScheme('light', {
|
||||||
getInitialValueInEffect: false,
|
getInitialValueInEffect: false,
|
||||||
});
|
});
|
||||||
|
|
@ -57,16 +59,19 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
||||||
setTerminalContent((prev) => {
|
setTerminalContent((prev) => {
|
||||||
const newData = data.toString();
|
const newData = data.toString();
|
||||||
|
|
||||||
if (
|
if (onServerReady) {
|
||||||
onServerReady &&
|
if (
|
||||||
newData.includes('Please connect to custom endpoint at ')
|
newData.includes('Please connect to custom endpoint at') ||
|
||||||
) {
|
newData.includes(
|
||||||
const match = newData.match(
|
'Now running KoboldCpp in Interactive Terminal Chat mode'
|
||||||
/Please connect to custom endpoint at (http:\/\/[^\s]+)/
|
)
|
||||||
);
|
) {
|
||||||
if (match) {
|
const serverHost = host || 'localhost';
|
||||||
const serverUrl = match[1];
|
const serverPort = port || 5001;
|
||||||
setTimeout(() => onServerReady(serverUrl), 1500);
|
setTimeout(
|
||||||
|
() => onServerReady(`http://${serverHost}:${serverPort}`),
|
||||||
|
1500
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +80,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [onServerReady]);
|
}, [onServerReady, host, port]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (viewportRef.current) {
|
if (viewportRef.current) {
|
||||||
|
|
@ -126,9 +131,10 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
}}
|
}}
|
||||||
>
|
dangerouslySetInnerHTML={{
|
||||||
{terminalContent}
|
__html: processTerminalContent(terminalContent),
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
import { ServerTab } from '@/components/screens/Interface/ServerTab';
|
||||||
import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
|
import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
|
||||||
import type { InterfaceTab } from '@/types';
|
import type { InterfaceTab, FrontendPreference } from '@/types';
|
||||||
|
|
||||||
interface InterfaceScreenProps {
|
interface InterfaceScreenProps {
|
||||||
activeTab?: InterfaceTab | null;
|
activeTab?: InterfaceTab | null;
|
||||||
|
|
@ -16,6 +16,26 @@ export const InterfaceScreen = ({
|
||||||
}: InterfaceScreenProps) => {
|
}: InterfaceScreenProps) => {
|
||||||
const [serverUrl, setServerUrl] = useState<string>('');
|
const [serverUrl, setServerUrl] = useState<string>('');
|
||||||
const [isServerReady, setIsServerReady] = useState<boolean>(false);
|
const [isServerReady, setIsServerReady] = useState<boolean>(false);
|
||||||
|
const [frontendPreference, setFrontendPreference] =
|
||||||
|
useState<FrontendPreference>('koboldcpp');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadFrontendPreference = async () => {
|
||||||
|
try {
|
||||||
|
const frontendPreference = (await window.electronAPI.config.get(
|
||||||
|
'frontendPreference'
|
||||||
|
)) as FrontendPreference;
|
||||||
|
setFrontendPreference(frontendPreference || 'koboldcpp');
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError(
|
||||||
|
'Failed to load frontend preference:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFrontendPreference();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleServerReady = useCallback(
|
const handleServerReady = useCallback(
|
||||||
(url: string) => {
|
(url: string) => {
|
||||||
|
|
@ -47,6 +67,7 @@ export const InterfaceScreen = ({
|
||||||
serverUrl={serverUrl}
|
serverUrl={serverUrl}
|
||||||
isServerReady={isServerReady}
|
isServerReady={isServerReady}
|
||||||
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
|
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
|
||||||
|
frontendPreference={frontendPreference}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import { Text, Group, Badge } from '@mantine/core';
|
import { Text, Group, Badge } from '@mantine/core';
|
||||||
|
import type { BackendOption } from '@/types';
|
||||||
|
|
||||||
interface BackendSelectItemProps {
|
type BackendSelectItemProps = Omit<BackendOption, 'value'>;
|
||||||
label: string;
|
|
||||||
devices?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BackendSelectItem = ({
|
export const BackendSelectItem = ({
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
|
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
|
||||||
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
|
||||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
|
import type { BackendOption } from '@/types';
|
||||||
|
|
||||||
export const BackendSelector = () => {
|
export const BackendSelector = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -15,88 +16,16 @@ export const BackendSelector = () => {
|
||||||
handleAutoGpuLayersChange,
|
handleAutoGpuLayersChange,
|
||||||
} = useLaunchConfig();
|
} = useLaunchConfig();
|
||||||
|
|
||||||
const [availableBackends, setAvailableBackends] = useState<
|
const [availableBackends, setAvailableBackends] = useState<BackendOption[]>(
|
||||||
Array<{
|
[]
|
||||||
value: string;
|
);
|
||||||
label: string;
|
|
||||||
devices?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
}>
|
|
||||||
>([]);
|
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadBackends = async () => {
|
const loadBackends = async () => {
|
||||||
try {
|
try {
|
||||||
const [cpuCapabilitiesResult, binarySupport, gpuCapabilities] =
|
const backends =
|
||||||
await Promise.all([
|
await window.electronAPI.kobold.getAvailableBackends(true);
|
||||||
window.electronAPI.kobold.detectCPU(),
|
|
||||||
window.electronAPI.kobold.detectBackendSupport(),
|
|
||||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!binarySupport) {
|
|
||||||
setAvailableBackends([]);
|
|
||||||
hasInitialized.current = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const backends: Array<{
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
devices?: string[];
|
|
||||||
disabled?: boolean;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
if (binarySupport.cuda) {
|
|
||||||
backends.push({
|
|
||||||
value: 'cuda',
|
|
||||||
label: 'CUDA',
|
|
||||||
devices: gpuCapabilities.cuda.devices,
|
|
||||||
disabled: !gpuCapabilities.cuda.supported,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binarySupport.rocm) {
|
|
||||||
backends.push({
|
|
||||||
value: 'rocm',
|
|
||||||
label: 'ROCm',
|
|
||||||
devices: gpuCapabilities.rocm.devices,
|
|
||||||
disabled: !gpuCapabilities.rocm.supported,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binarySupport.vulkan) {
|
|
||||||
backends.push({
|
|
||||||
value: 'vulkan',
|
|
||||||
label: 'Vulkan',
|
|
||||||
devices: gpuCapabilities.vulkan.devices,
|
|
||||||
disabled: !gpuCapabilities.vulkan.supported,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binarySupport.clblast) {
|
|
||||||
backends.push({
|
|
||||||
value: 'clblast',
|
|
||||||
label: 'CLBlast',
|
|
||||||
devices: gpuCapabilities.clblast.devices,
|
|
||||||
disabled: !gpuCapabilities.clblast.supported,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
backends.push({
|
|
||||||
value: 'cpu',
|
|
||||||
label: 'CPU',
|
|
||||||
devices: cpuCapabilitiesResult.devices,
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
backends.sort((a, b) => {
|
|
||||||
if (a.disabled === b.disabled) return 0;
|
|
||||||
return a.disabled ? 1 : -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
setAvailableBackends(backends);
|
setAvailableBackends(backends);
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { Text, Group, Select, TextInput } from '@mantine/core';
|
import { Text, Group, Select, TextInput } from '@mantine/core';
|
||||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
|
import type { BackendOption } from '@/types';
|
||||||
|
|
||||||
interface GpuDeviceSelectorProps {
|
interface GpuDeviceSelectorProps {
|
||||||
availableBackends: Array<{
|
availableBackends: BackendOption[];
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
devices?: string[];
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GpuDeviceSelector = ({
|
export const GpuDeviceSelector = ({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import { Stack, Text, Group, SegmentedControl, rem } from '@mantine/core';
|
import {
|
||||||
import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core';
|
Stack,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
SegmentedControl,
|
||||||
|
rem,
|
||||||
|
useMantineColorScheme,
|
||||||
|
useComputedColorScheme,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||||
|
|
||||||
export const AppearanceTab = () => {
|
export const AppearanceTab = () => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
|
import {
|
||||||
import { Folder, FolderOpen } from 'lucide-react';
|
Stack,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
TextInput,
|
||||||
|
Button,
|
||||||
|
rem,
|
||||||
|
Select,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Folder, FolderOpen, Monitor } from 'lucide-react';
|
||||||
import styles from '@/styles/layout.module.css';
|
import styles from '@/styles/layout.module.css';
|
||||||
|
import type { FrontendPreference } from '@/types';
|
||||||
|
import { FRONTENDS } from '@/constants';
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
export const GeneralTab = () => {
|
||||||
const [installDir, setInstallDir] = useState<string>('');
|
const [installDir, setInstallDir] = useState<string>('');
|
||||||
|
const [FrontendPreference, setFrontendPreference] =
|
||||||
|
useState<FrontendPreference>('koboldcpp');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCurrentInstallDir();
|
loadCurrentInstallDir();
|
||||||
|
loadFrontendPreference();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadCurrentInstallDir = async () => {
|
const loadCurrentInstallDir = async () => {
|
||||||
|
|
@ -22,6 +35,20 @@ export const GeneralTab = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadFrontendPreference = async () => {
|
||||||
|
try {
|
||||||
|
const frontendPreference = (await window.electronAPI.config.get(
|
||||||
|
'frontendPreference'
|
||||||
|
)) as FrontendPreference;
|
||||||
|
setFrontendPreference(frontendPreference || 'koboldcpp');
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError(
|
||||||
|
'Failed to load frontend preference:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectInstallDir = async () => {
|
const handleSelectInstallDir = async () => {
|
||||||
try {
|
try {
|
||||||
const selectedDir =
|
const selectedDir =
|
||||||
|
|
@ -38,6 +65,20 @@ export const GeneralTab = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFrontendPreferenceChange = async (value: string | null) => {
|
||||||
|
if (!value || (value !== 'koboldcpp' && value !== 'sillytavern')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electronAPI.config.set('frontendPreference', value);
|
||||||
|
setFrontendPreference(value);
|
||||||
|
} catch (error) {
|
||||||
|
window.electronAPI.logs.logError(
|
||||||
|
'Failed to save frontend preference:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg" h="100%">
|
<Stack gap="lg" h="100%">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -66,6 +107,27 @@ export const GeneralTab = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text fw={500} mb="sm">
|
||||||
|
Frontend Interface
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Choose which frontend interface to use for interacting with models
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
value={FrontendPreference}
|
||||||
|
onChange={handleFrontendPreferenceChange}
|
||||||
|
data={[
|
||||||
|
{ value: 'koboldcpp', label: 'KoboldCpp (Built-in)' },
|
||||||
|
{
|
||||||
|
value: 'sillytavern',
|
||||||
|
label: FRONTENDS.SILLYTAVERN,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@ import { Settings, Palette, SlidersHorizontal, GitBranch } from 'lucide-react';
|
||||||
import { GeneralTab } from '@/components/settings/GeneralTab';
|
import { GeneralTab } from '@/components/settings/GeneralTab';
|
||||||
import { VersionsTab } from '@/components/settings/VersionsTab';
|
import { VersionsTab } from '@/components/settings/VersionsTab';
|
||||||
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
import { AppearanceTab } from '@/components/settings/AppearanceTab';
|
||||||
|
import type { Screen } from '@/types';
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
currentScreen?: 'welcome' | 'download' | 'launch' | 'interface';
|
currentScreen?: Screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsModal = ({
|
export const SettingsModal = ({
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,14 @@ export const DEFAULT_CONTEXT_SIZE = 4096;
|
||||||
|
|
||||||
export const DEFAULT_MODEL_URL =
|
export const DEFAULT_MODEL_URL =
|
||||||
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true';
|
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true';
|
||||||
|
|
||||||
|
export const SILLYTAVERN = {
|
||||||
|
PORT: 3000,
|
||||||
|
PROXY_PORT: 3001,
|
||||||
|
get URL() {
|
||||||
|
return `http://localhost:${this.PORT}`;
|
||||||
|
},
|
||||||
|
get PROXY_URL() {
|
||||||
|
return `http://localhost:${this.PROXY_PORT}`;
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export const APP_NAME = 'friendly-kobold';
|
export const APP_NAME = 'friendly-kobold';
|
||||||
|
export const PRODUCT_NAME = 'Friendly Kobold';
|
||||||
|
|
||||||
export const CONFIG_FILE_NAME = 'config.json';
|
export const CONFIG_FILE_NAME = 'config.json';
|
||||||
|
|
||||||
|
|
@ -32,14 +33,14 @@ export const ASSET_SUFFIXES = {
|
||||||
OLDPC: 'oldpc',
|
OLDPC: 'oldpc',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const KOBOLDAI_URLS = {
|
|
||||||
STANDARD_DOWNLOAD: 'https://koboldai.org/cpp',
|
|
||||||
ROCM_DOWNLOAD: 'https://koboldai.org/cpplinuxrocm',
|
|
||||||
DOMAIN: 'koboldai.org',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const ROCM = {
|
export const ROCM = {
|
||||||
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||||
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
|
DOWNLOAD_URL: 'https://koboldai.org/cpplinuxrocm',
|
||||||
SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
|
SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const FRONTENDS = {
|
||||||
|
KOBOLDAI_LITE: 'KoboldAI Lite',
|
||||||
|
STABLE_UI: 'Stable UI',
|
||||||
|
SILLYTAVERN: 'SillyTavern',
|
||||||
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { parseCLBlastDevice } from '@/utils';
|
|
||||||
import type { SdConvDirectMode } from '@/types';
|
import type { SdConvDirectMode } from '@/types';
|
||||||
|
|
||||||
interface UseLaunchLogicProps {
|
interface UseLaunchLogicProps {
|
||||||
|
|
@ -105,7 +104,7 @@ const buildConfigArgs = (
|
||||||
args.push('--host', launchArgs.host);
|
args.push('--host', launchArgs.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
const flagMappings: Array<[boolean, string, string?]> = [
|
const flagMappings: [boolean, string, string?][] = [
|
||||||
[launchArgs.multiuser, '--multiuser', '1'],
|
[launchArgs.multiuser, '--multiuser', '1'],
|
||||||
[launchArgs.multiplayer, '--multiplayer'],
|
[launchArgs.multiplayer, '--multiplayer'],
|
||||||
[launchArgs.remotetunnel, '--remotetunnel'],
|
[launchArgs.remotetunnel, '--remotetunnel'],
|
||||||
|
|
@ -214,6 +213,20 @@ const buildBackendArgs = (launchArgs: LaunchArgs): string[] => {
|
||||||
return args;
|
return args;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function parseCLBlastDevice(deviceString: string): {
|
||||||
|
deviceIndex: number;
|
||||||
|
platformIndex: number;
|
||||||
|
} | null {
|
||||||
|
const match = deviceString.match(/\[(\d+),(\d+)\]$/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
deviceIndex: parseInt(match[1], 10),
|
||||||
|
platformIndex: parseInt(match[2], 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export const useLaunchLogic = ({
|
export const useLaunchLogic = ({
|
||||||
modelPath,
|
modelPath,
|
||||||
sdmodel,
|
sdmodel,
|
||||||
|
|
@ -233,6 +246,8 @@ export const useLaunchLogic = ({
|
||||||
|
|
||||||
setIsLaunching(true);
|
setIsLaunching(true);
|
||||||
|
|
||||||
|
onLaunch();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const args: string[] = [
|
const args: string[] = [
|
||||||
...buildModelArgs(isImageMode, modelPath, sdmodel, launchArgs),
|
...buildModelArgs(isImageMode, modelPath, sdmodel, launchArgs),
|
||||||
|
|
@ -253,10 +268,6 @@ export const useLaunchLogic = ({
|
||||||
if (onLaunchModeChange) {
|
if (onLaunchModeChange) {
|
||||||
onLaunchModeChange(isImageMode);
|
onLaunchModeChange(isImageMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
onLaunch();
|
|
||||||
}, 100);
|
|
||||||
} else {
|
} else {
|
||||||
window.electronAPI.logs.logError(
|
window.electronAPI.logs.logError(
|
||||||
'Launch failed:',
|
'Launch failed:',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo, useEffect, useState, useCallback } from 'react';
|
import { useMemo, useEffect, useState, useCallback } from 'react';
|
||||||
|
import type { BackendOption } from '@/types';
|
||||||
|
|
||||||
export interface Warning {
|
export interface Warning {
|
||||||
type: 'warning' | 'info';
|
type: 'warning' | 'info';
|
||||||
|
|
@ -135,7 +136,7 @@ const checkCpuWarnings = (
|
||||||
cpuCapabilities: { avx: boolean; avx2: boolean },
|
cpuCapabilities: { avx: boolean; avx2: boolean },
|
||||||
noavx2: boolean,
|
noavx2: boolean,
|
||||||
failsafe: boolean,
|
failsafe: boolean,
|
||||||
availableBackends: Array<{ value: string; label: string; devices?: string[] }>
|
availableBackends: BackendOption[]
|
||||||
): Warning[] => {
|
): Warning[] => {
|
||||||
const warnings: Warning[] = [];
|
const warnings: Warning[] = [];
|
||||||
|
|
||||||
|
|
@ -181,11 +182,7 @@ const checkBackendWarnings = async (params?: {
|
||||||
} | null;
|
} | null;
|
||||||
noavx2: boolean;
|
noavx2: boolean;
|
||||||
failsafe: boolean;
|
failsafe: boolean;
|
||||||
availableBackends: Array<{
|
availableBackends: BackendOption[];
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
devices?: string[];
|
|
||||||
}>;
|
|
||||||
}): Promise<Warning[]> => {
|
}): Promise<Warning[]> => {
|
||||||
const warnings: Warning[] = [];
|
const warnings: Warning[] = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,28 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { existsSync, readFileSync } from 'fs';
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
|
||||||
const CONFIG_FILE_NAME = 'config.json';
|
|
||||||
|
|
||||||
export class LightweightCliHandler {
|
export class LightweightCliHandler {
|
||||||
private getConfigPath(): string {
|
private getConfigDir(appName: string): string {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'win32':
|
case 'win32':
|
||||||
return join(
|
return join(home, 'AppData', 'Roaming', appName);
|
||||||
home,
|
|
||||||
'AppData',
|
|
||||||
'Roaming',
|
|
||||||
'Friendly Kobold',
|
|
||||||
CONFIG_FILE_NAME
|
|
||||||
);
|
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
return join(
|
return join(home, 'Library', 'Application Support', appName);
|
||||||
home,
|
|
||||||
'Library',
|
|
||||||
'Application Support',
|
|
||||||
'Friendly Kobold',
|
|
||||||
CONFIG_FILE_NAME
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return join(home, '.config', 'Friendly Kobold', CONFIG_FILE_NAME);
|
return join(home, '.config', appName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getConfigPath(): string {
|
||||||
|
return join(this.getConfigDir(PRODUCT_NAME), CONFIG_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
private getCurrentKoboldBinary(): string | null {
|
private getCurrentKoboldBinary(): string | null {
|
||||||
try {
|
try {
|
||||||
const configPath = this.getConfigPath();
|
const configPath = this.getConfigPath();
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
import { homedir } from 'os';
|
|
||||||
|
|
||||||
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 { GitHubService } from '@/main/services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { HardwareService } from '@/main/services/HardwareService';
|
import { HardwareService } from '@/main/services/HardwareService';
|
||||||
import { BinaryService } from '@/main/services/BinaryService';
|
import { BinaryService } from '@/main/services/BinaryService';
|
||||||
import { IPCHandlers } from '@/main/utils/IPCHandlers';
|
import { IPCHandlers } from '@/main/ipc';
|
||||||
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants';
|
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
|
||||||
export class FriendlyKoboldApp {
|
export class FriendlyKoboldApp {
|
||||||
private windowManager: WindowManager;
|
private windowManager: WindowManager;
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private logManager: LogManager;
|
private logManager: LogManager;
|
||||||
private koboldManager: KoboldCppManager;
|
private koboldManager: KoboldCppManager;
|
||||||
|
private sillyTavernManager: SillyTavernManager;
|
||||||
private githubService: GitHubService;
|
private githubService: GitHubService;
|
||||||
private hardwareService: HardwareService;
|
private hardwareService: HardwareService;
|
||||||
private binaryService: BinaryService;
|
private binaryService: BinaryService;
|
||||||
|
|
@ -49,13 +51,19 @@ export class FriendlyKoboldApp {
|
||||||
this.hardwareService
|
this.hardwareService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.sillyTavernManager = new SillyTavernManager(
|
||||||
|
this.logManager,
|
||||||
|
this.windowManager
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcHandlers = new IPCHandlers(
|
this.ipcHandlers = new IPCHandlers(
|
||||||
this.koboldManager,
|
this.koboldManager,
|
||||||
this.configManager,
|
this.configManager,
|
||||||
this.githubService,
|
this.githubService,
|
||||||
this.hardwareService,
|
this.hardwareService,
|
||||||
this.binaryService,
|
this.binaryService,
|
||||||
this.logManager
|
this.logManager,
|
||||||
|
this.sillyTavernManager
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,23 +71,24 @@ export class FriendlyKoboldApp {
|
||||||
return join(app.getPath('userData'), CONFIG_FILE_NAME);
|
return join(app.getPath('userData'), CONFIG_FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultInstallPath() {
|
private getDefaultInstallDir(appName: string): string {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
const home = homedir();
|
const home = homedir();
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'win32':
|
case 'win32':
|
||||||
return join(home, APP_NAME);
|
return join(home, appName);
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
return join(home, 'Applications', APP_NAME);
|
return join(home, 'Applications', appName);
|
||||||
default:
|
default:
|
||||||
return join(home, '.local', 'share', APP_NAME);
|
return join(home, '.local', 'share', appName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureInstallDirectory() {
|
private ensureInstallDirectory() {
|
||||||
const installDir =
|
const installDir =
|
||||||
this.configManager.getInstallDir() || this.getDefaultInstallPath();
|
this.configManager.getInstallDir() ||
|
||||||
|
this.getDefaultInstallDir(PRODUCT_NAME);
|
||||||
|
|
||||||
if (!this.configManager.getInstallDir()) {
|
if (!this.configManager.getInstallDir()) {
|
||||||
this.configManager.setInstallDir(installDir);
|
this.configManager.setInstallDir(installDir);
|
||||||
|
|
@ -113,19 +122,20 @@ export class FriendlyKoboldApp {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cleanupPromise = this.koboldManager.cleanup();
|
const cleanupPromises = [
|
||||||
|
this.koboldManager.cleanup(),
|
||||||
|
this.sillyTavernManager.cleanup(),
|
||||||
|
];
|
||||||
|
|
||||||
const timeoutPromise = new Promise<void>((resolve) => {
|
const timeoutPromise = new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.race([cleanupPromise, timeoutPromise]);
|
await Promise.race([Promise.all(cleanupPromises), timeoutPromise]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logManager.logError(
|
this.logManager.logError('Error during cleanup:', error as Error);
|
||||||
'Error during KoboldCpp cleanup:',
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.windowManager.cleanup();
|
this.windowManager.cleanup();
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain, shell, app } from 'electron';
|
||||||
import { shell, app } from 'electron';
|
|
||||||
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||||
import { LogManager } from '@/main/managers/LogManager';
|
import { LogManager } from '@/main/managers/LogManager';
|
||||||
|
import { SillyTavernManager } from '@/main/managers/SillyTavernManager';
|
||||||
import { GitHubService } from '@/main/services/GitHubService';
|
import { GitHubService } from '@/main/services/GitHubService';
|
||||||
import { HardwareService } from '@/main/services/HardwareService';
|
import { HardwareService } from '@/main/services/HardwareService';
|
||||||
import { BinaryService } from '@/main/services/BinaryService';
|
import { BinaryService } from '@/main/services/BinaryService';
|
||||||
|
import type { FrontendPreference } from '@/types';
|
||||||
|
|
||||||
export class IPCHandlers {
|
export class IPCHandlers {
|
||||||
private koboldManager: KoboldCppManager;
|
private koboldManager: KoboldCppManager;
|
||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private logManager: LogManager;
|
private logManager: LogManager;
|
||||||
|
private sillyTavernManager: SillyTavernManager;
|
||||||
private githubService: GitHubService;
|
private githubService: GitHubService;
|
||||||
private hardwareService: HardwareService;
|
private hardwareService: HardwareService;
|
||||||
private binaryService: BinaryService;
|
private binaryService: BinaryService;
|
||||||
|
|
@ -21,16 +23,51 @@ export class IPCHandlers {
|
||||||
githubService: GitHubService,
|
githubService: GitHubService,
|
||||||
hardwareService: HardwareService,
|
hardwareService: HardwareService,
|
||||||
binaryService: BinaryService,
|
binaryService: BinaryService,
|
||||||
logManager: LogManager
|
logManager: LogManager,
|
||||||
|
sillyTavernManager: SillyTavernManager
|
||||||
) {
|
) {
|
||||||
this.koboldManager = koboldManager;
|
this.koboldManager = koboldManager;
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
this.logManager = logManager;
|
this.logManager = logManager;
|
||||||
|
this.sillyTavernManager = sillyTavernManager;
|
||||||
this.githubService = githubService;
|
this.githubService = githubService;
|
||||||
this.hardwareService = hardwareService;
|
this.hardwareService = hardwareService;
|
||||||
this.binaryService = binaryService;
|
this.binaryService = binaryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async launchKoboldCppWithCustomFrontends(
|
||||||
|
args: string[] = []
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
pid?: number;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const frontendPreference = (await this.configManager.get(
|
||||||
|
'frontendPreference'
|
||||||
|
)) as FrontendPreference | undefined;
|
||||||
|
|
||||||
|
if (frontendPreference === 'sillytavern') {
|
||||||
|
try {
|
||||||
|
await this.sillyTavernManager.startFrontend(args);
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError(
|
||||||
|
'Failed to setup SillyTavern text frontend:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.koboldManager.launchKoboldCpp(args);
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError('Error in enhanced launch:', error as Error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupHandlers() {
|
setupHandlers() {
|
||||||
ipcMain.handle('kobold:getLatestRelease', () =>
|
ipcMain.handle('kobold:getLatestRelease', () =>
|
||||||
this.githubService.getLatestRelease()
|
this.githubService.getLatestRelease()
|
||||||
|
|
@ -128,8 +165,10 @@ export class IPCHandlers {
|
||||||
this.binaryService.detectBackendSupport()
|
this.binaryService.detectBackendSupport()
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getAvailableBackends', () =>
|
ipcMain.handle(
|
||||||
this.binaryService.getAvailableBackends()
|
'kobold:getAvailableBackends',
|
||||||
|
(_event, includeDisabled = false) =>
|
||||||
|
this.binaryService.getAvailableBackends(includeDisabled)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getPlatform', () => ({
|
ipcMain.handle('kobold:getPlatform', () => ({
|
||||||
|
|
@ -170,7 +209,7 @@ export class IPCHandlers {
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:launchKoboldCpp', (_event, args) =>
|
ipcMain.handle('kobold:launchKoboldCpp', (_event, args) =>
|
||||||
this.koboldManager.launchKoboldCpp(args)
|
this.launchKoboldCppWithCustomFrontends(args)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:stopKoboldCpp', () =>
|
ipcMain.handle('kobold:stopKoboldCpp', () =>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
import { LogManager } from '@/main/managers/LogManager';
|
import { LogManager } from '@/main/managers/LogManager';
|
||||||
|
import type { FrontendPreference } from '@/types';
|
||||||
|
|
||||||
type ConfigValue = string | number | boolean | unknown[] | undefined;
|
type ConfigValue = string | number | boolean | unknown[] | undefined;
|
||||||
|
|
||||||
|
|
@ -7,6 +8,7 @@ interface AppConfig {
|
||||||
installDir?: string;
|
installDir?: string;
|
||||||
currentKoboldBinary?: string;
|
currentKoboldBinary?: string;
|
||||||
selectedConfig?: string;
|
selectedConfig?: string;
|
||||||
|
frontendPreference?: FrontendPreference;
|
||||||
[key: string]: ConfigValue;
|
[key: string]: ConfigValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable no-comments/disallowComments */
|
/* eslint-disable no-comments/disallowComments */
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, ChildProcess, exec as execCmd } from 'child_process';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import {
|
import {
|
||||||
existsSync,
|
existsSync,
|
||||||
|
|
@ -20,50 +20,16 @@ import { GitHubService } from '@/main/services/GitHubService';
|
||||||
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 { WindowManager } from '@/main/managers/WindowManager';
|
import { WindowManager } from '@/main/managers/WindowManager';
|
||||||
import { ROCM } from '@/constants';
|
import { ROCM, PRODUCT_NAME } from '@/constants';
|
||||||
import { stripAssetExtensions } from '@/utils/versionUtils';
|
import { stripAssetExtensions } from '@/utils/versionUtils';
|
||||||
import { compareVersions } from '@/utils';
|
import { compareVersions } from '@/utils';
|
||||||
import type { DownloadItem } from '@/types/electron';
|
import type {
|
||||||
|
DownloadItem,
|
||||||
interface GitHubAsset {
|
GitHubAsset,
|
||||||
name: string;
|
UpdateInfo,
|
||||||
browser_download_url: string;
|
ReleaseWithStatus,
|
||||||
size: number;
|
InstalledVersion,
|
||||||
created_at: string;
|
} from '@/types/electron';
|
||||||
isUpdate?: boolean;
|
|
||||||
wasCurrentBinary?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GitHubRelease {
|
|
||||||
tag_name: string;
|
|
||||||
name: string;
|
|
||||||
published_at: string;
|
|
||||||
body: string;
|
|
||||||
assets: GitHubAsset[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateInfo {
|
|
||||||
currentVersion: string;
|
|
||||||
latestVersion: string;
|
|
||||||
releaseInfo: GitHubRelease;
|
|
||||||
hasUpdate: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReleaseWithStatus {
|
|
||||||
release: GitHubRelease;
|
|
||||||
availableAssets: Array<{
|
|
||||||
asset: GitHubAsset;
|
|
||||||
isDownloaded: boolean;
|
|
||||||
installedVersion?: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InstalledVersion {
|
|
||||||
version: string;
|
|
||||||
path: string;
|
|
||||||
filename: string;
|
|
||||||
size?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class KoboldCppManager {
|
export class KoboldCppManager {
|
||||||
private installDir: string;
|
private installDir: string;
|
||||||
|
|
@ -155,10 +121,7 @@ export class KoboldCppManager {
|
||||||
this.configManager.setCurrentKoboldBinary(launcherPath);
|
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
this.windowManager.sendToRenderer('versions-updated');
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('versions-updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
return launcherPath;
|
return launcherPath;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -212,8 +175,7 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = readdirSync(this.installDir);
|
const items = readdirSync(this.installDir);
|
||||||
const launchers: Array<{ 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(this.installDir, item);
|
||||||
|
|
@ -269,9 +231,9 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfigFiles(): Promise<
|
async getConfigFiles(): Promise<
|
||||||
Array<{ name: string; path: string; size: number }>
|
{ name: string; path: string; size: number }[]
|
||||||
> {
|
> {
|
||||||
const configFiles: Array<{ name: string; path: string; size: number }> = [];
|
const configFiles: { name: string; path: string; size: number }[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (existsSync(this.installDir)) {
|
if (existsSync(this.installDir)) {
|
||||||
|
|
@ -452,10 +414,7 @@ export class KoboldCppManager {
|
||||||
if (existsSync(binaryPath)) {
|
if (existsSync(binaryPath)) {
|
||||||
this.configManager.setCurrentKoboldBinary(binaryPath);
|
this.configManager.setCurrentKoboldBinary(binaryPath);
|
||||||
|
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
this.windowManager.sendToRenderer('versions-updated');
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('versions-updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -511,7 +470,7 @@ export class KoboldCppManager {
|
||||||
async selectInstallDirectory(): Promise<string | null> {
|
async selectInstallDirectory(): Promise<string | null> {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
title: 'Select the Friendly Kobold Installation Directory',
|
title: `Select the ${PRODUCT_NAME} Installation Directory`,
|
||||||
defaultPath: this.installDir,
|
defaultPath: this.installDir,
|
||||||
buttonLabel: 'Select Directory',
|
buttonLabel: 'Select Directory',
|
||||||
});
|
});
|
||||||
|
|
@ -520,10 +479,10 @@ export class KoboldCppManager {
|
||||||
this.installDir = result.filePaths[0];
|
this.installDir = result.filePaths[0];
|
||||||
this.configManager.setInstallDir(result.filePaths[0]);
|
this.configManager.setInstallDir(result.filePaths[0]);
|
||||||
|
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
this.windowManager.sendToRenderer(
|
||||||
if (mainWindow) {
|
'install-dir-changed',
|
||||||
mainWindow.webContents.send('install-dir-changed', result.filePaths[0]);
|
result.filePaths[0]
|
||||||
}
|
);
|
||||||
|
|
||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
}
|
}
|
||||||
|
|
@ -715,10 +674,7 @@ export class KoboldCppManager {
|
||||||
this.configManager.setCurrentKoboldBinary(launcherPath);
|
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
this.windowManager.sendToRenderer('versions-updated');
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('versions-updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -846,59 +802,45 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
this.koboldProcess = child;
|
this.koboldProcess = child;
|
||||||
|
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}`;
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}\n${'─'.repeat(60)}\n`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
this.windowManager.sendKoboldOutput(commandLine);
|
||||||
mainWindow.webContents.send('kobold-output', commandLine);
|
}, 200);
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
child.stdout?.on('data', (data) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
const output = data.toString();
|
||||||
const output = data.toString();
|
this.windowManager.sendKoboldOutput(output, true);
|
||||||
mainWindow.webContents.send('kobold-output', output);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
child.stderr?.on('data', (data) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
const output = data.toString();
|
||||||
const output = data.toString();
|
this.windowManager.sendKoboldOutput(output, true);
|
||||||
mainWindow.webContents.send('kobold-output', output);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
const displayMessage = signal
|
||||||
const displayMessage = signal
|
? `\n[INFO] Process terminated with signal ${signal}`
|
||||||
? `\n[INFO] Process terminated with signal ${signal}\n`
|
: code === 0
|
||||||
: code === 0
|
? `\n[INFO] Process exited successfully`
|
||||||
? `\n[INFO] Process exited successfully\n`
|
: code && (code > 1 || code < 0)
|
||||||
: code && (code > 1 || code < 0)
|
? `\n[ERROR] Process exited with code ${code}`
|
||||||
? `\n[ERROR] Process exited with code ${code}\n`
|
: `\n[INFO] Process exited with code ${code}`;
|
||||||
: `\n[INFO] Process exited with code ${code}\n`;
|
this.windowManager.sendKoboldOutput(displayMessage);
|
||||||
mainWindow.webContents.send('kobold-output', displayMessage);
|
this.koboldProcess = null;
|
||||||
}
|
});
|
||||||
this.koboldProcess = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
this.logManager.logError(
|
this.logManager.logError(
|
||||||
`KoboldCpp process error: ${error.message}`,
|
`KoboldCpp process error: ${error.message}`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
this.windowManager.sendKoboldOutput(
|
||||||
mainWindow.webContents.send(
|
`\n[ERROR] Process error: ${error.message}\n`
|
||||||
'kobold-output',
|
);
|
||||||
`\n[ERROR] Process error: ${error.message}\n`
|
this.koboldProcess = null;
|
||||||
);
|
});
|
||||||
}
|
|
||||||
this.koboldProcess = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, pid: child.pid };
|
return { success: true, pid: child.pid };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -967,7 +909,6 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||||
const { exec: execCmd } = require('child_process');
|
|
||||||
execCmd(
|
execCmd(
|
||||||
`taskkill /pid ${pid} /t /f`,
|
`taskkill /pid ${pid} /t /f`,
|
||||||
(error: Error | null) => {
|
(error: Error | null) => {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ export class LogManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public logDebug(message: string) {
|
public logDebug(message: string) {
|
||||||
this.logger.debug(message);
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setupGlobalErrorHandlers() {
|
public setupGlobalErrorHandlers() {
|
||||||
|
|
|
||||||
452
src/main/managers/SillyTavernManager.ts
Normal file
452
src/main/managers/SillyTavernManager.ts
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { createServer, request, type Server } from 'http';
|
||||||
|
import { app } from 'electron';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
|
import { LogManager } from './LogManager';
|
||||||
|
import { WindowManager } from './WindowManager';
|
||||||
|
import { SILLYTAVERN } from '@/constants';
|
||||||
|
import { getPlatformPathFromWindowsPath } from '@/utils/server';
|
||||||
|
|
||||||
|
export interface SillyTavernConfig {
|
||||||
|
name: string;
|
||||||
|
port: number;
|
||||||
|
proxyPort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SillyTavernManager {
|
||||||
|
private sillyTavernProcess: ChildProcess | null = null;
|
||||||
|
private proxyServer: Server | null = null;
|
||||||
|
private logManager: LogManager;
|
||||||
|
private windowManager: WindowManager;
|
||||||
|
|
||||||
|
constructor(logManager: LogManager, windowManager: WindowManager) {
|
||||||
|
this.logManager = logManager;
|
||||||
|
this.windowManager = windowManager;
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
this.cleanup().catch(() => {
|
||||||
|
void 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
this.cleanup().catch(() => {
|
||||||
|
void 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSillyTavernSettingsPath(): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
const home = homedir();
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
return join(
|
||||||
|
home,
|
||||||
|
'AppData',
|
||||||
|
'Roaming',
|
||||||
|
'SillyTavern',
|
||||||
|
'data',
|
||||||
|
'default-user',
|
||||||
|
'settings.json'
|
||||||
|
);
|
||||||
|
case 'darwin':
|
||||||
|
return join(
|
||||||
|
home,
|
||||||
|
'Library',
|
||||||
|
'Application Support',
|
||||||
|
'SillyTavern',
|
||||||
|
'data',
|
||||||
|
'default-user',
|
||||||
|
'settings.json'
|
||||||
|
);
|
||||||
|
case 'linux':
|
||||||
|
default:
|
||||||
|
return join(
|
||||||
|
home,
|
||||||
|
'.local',
|
||||||
|
'share',
|
||||||
|
'SillyTavern',
|
||||||
|
'data',
|
||||||
|
'default-user',
|
||||||
|
'settings.json'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSillyTavernSettings(): Promise<void> {
|
||||||
|
const settingsPath = this.getSillyTavernSettingsPath();
|
||||||
|
|
||||||
|
if (existsSync(settingsPath)) {
|
||||||
|
this.windowManager.sendKoboldOutput('SillyTavern settings found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
'SillyTavern settings not found, starting SillyTavern briefly to generate config...'
|
||||||
|
);
|
||||||
|
|
||||||
|
const bundledNpxPath = getPlatformPathFromWindowsPath(
|
||||||
|
app.getAppPath(),
|
||||||
|
'npx.cmd'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(`Using bundled npx: ${bundledNpxPath}`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const initProcess = spawn(
|
||||||
|
bundledNpxPath,
|
||||||
|
['sillytavern', '--browserLaunchEnabled', 'false'],
|
||||||
|
{
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
detached: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let hasResolved = false;
|
||||||
|
|
||||||
|
const cleanupAndResolve = () => {
|
||||||
|
if (!hasResolved) {
|
||||||
|
hasResolved = true;
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
'SillyTavern settings should now be generated'
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (!initProcess.killed) {
|
||||||
|
initProcess.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
cleanupAndResolve();
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
initProcess.on('exit', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
cleanupAndResolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
initProcess.on('error', (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (!hasResolved) {
|
||||||
|
hasResolved = true;
|
||||||
|
this.logManager.logError(
|
||||||
|
'Failed to initialize SillyTavern settings:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initProcess.stdout) {
|
||||||
|
initProcess.stdout.on('data', (data: Buffer) => {
|
||||||
|
const output = data.toString();
|
||||||
|
if (output.includes('SillyTavern is listening')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!initProcess.killed && !hasResolved) {
|
||||||
|
initProcess.kill('SIGTERM');
|
||||||
|
cleanupAndResolve();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!initProcess.killed && !hasResolved) {
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
'SillyTavern initialization taking longer than expected, proceeding...'
|
||||||
|
);
|
||||||
|
initProcess.kill('SIGTERM');
|
||||||
|
cleanupAndResolve();
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseKoboldConfig(args: string[]): { host: string; port: number } {
|
||||||
|
let host = 'localhost';
|
||||||
|
let port = 5001;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { host, port };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupSillyTavernConfig(
|
||||||
|
koboldHost: string,
|
||||||
|
koboldPort: number
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const configPath = this.getSillyTavernSettingsPath();
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Configuring SillyTavern settings at: ${configPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
let settings: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(configPath, 'utf-8');
|
||||||
|
settings = JSON.parse(content) as Record<string, unknown>;
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Loaded existing SillyTavern settings`
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Could not read existing settings, creating new ones`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const koboldUrl = `http://${koboldHost}:${koboldPort}`;
|
||||||
|
const koboldApiUrl = `${koboldUrl}/api`;
|
||||||
|
|
||||||
|
if (!settings.power_user) settings.power_user = {};
|
||||||
|
const powerUser = settings.power_user as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (!settings.textgenerationwebui_settings)
|
||||||
|
settings.textgenerationwebui_settings = {};
|
||||||
|
const textgenSettings = settings.textgenerationwebui_settings as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (!textgenSettings.server_urls) textgenSettings.server_urls = {};
|
||||||
|
const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
|
||||||
|
serverUrls.koboldcpp = koboldUrl;
|
||||||
|
|
||||||
|
settings.main_api = 'textgenerationwebui';
|
||||||
|
textgenSettings.type = 'koboldcpp';
|
||||||
|
powerUser.auto_connect = true;
|
||||||
|
|
||||||
|
writeFileSync(configPath, JSON.stringify(settings, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`SillyTavern configuration updated successfully!`
|
||||||
|
);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`KoboldCpp will auto-connect to: ${koboldApiUrl}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError(
|
||||||
|
'Failed to setup SillyTavern config:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Failed to configure SillyTavern: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createProxyServer(targetPort: number, proxyPort: number): void {
|
||||||
|
this.proxyServer = createServer((req, res) => {
|
||||||
|
const options = {
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: targetPort,
|
||||||
|
path: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyReq = request(options, (proxyRes) => {
|
||||||
|
const headers = { ...proxyRes.headers };
|
||||||
|
delete headers['x-frame-options'];
|
||||||
|
res.writeHead(proxyRes.statusCode || 200, headers);
|
||||||
|
proxyRes.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyReq.on('error', (err) => {
|
||||||
|
this.logManager.logError('Proxy request error:', err);
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end('Proxy error');
|
||||||
|
});
|
||||||
|
|
||||||
|
req.pipe(proxyReq);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proxyServer.listen(proxyPort, () => {
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Proxy server started on port ${proxyPort}, forwarding to SillyTavern on port ${targetPort}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.proxyServer.on('error', (err) => {
|
||||||
|
this.logManager.logError('Proxy server error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultSillyTavernConfig(): SillyTavernConfig {
|
||||||
|
return {
|
||||||
|
name: 'sillytavern',
|
||||||
|
port: SILLYTAVERN.PORT,
|
||||||
|
proxyPort: SILLYTAVERN.PROXY_PORT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async startFrontend(args: string[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config = this.getDefaultSillyTavernConfig();
|
||||||
|
const { host: koboldHost, port: koboldPort } =
|
||||||
|
this.parseKoboldConfig(args);
|
||||||
|
|
||||||
|
await this.stopFrontend();
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Preparing SillyTavern to connect to KoboldCpp at ${koboldHost}:${koboldPort}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.ensureSillyTavernSettings();
|
||||||
|
await this.setupSillyTavernConfig(koboldHost, koboldPort);
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Starting ${config.name} frontend on port ${config.port}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const sillyTavernArgs = [
|
||||||
|
'sillytavern',
|
||||||
|
'--port',
|
||||||
|
config.port.toString(),
|
||||||
|
'--listen',
|
||||||
|
'--corsProxy',
|
||||||
|
'--securityOverride',
|
||||||
|
'--disableCsrf',
|
||||||
|
'--browserLaunchEnabled',
|
||||||
|
'false',
|
||||||
|
];
|
||||||
|
|
||||||
|
const bundledNpxPath = getPlatformPathFromWindowsPath(
|
||||||
|
app.getAppPath(),
|
||||||
|
'npx.cmd'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`Using bundled npx: ${bundledNpxPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sillyTavernProcess = spawn(bundledNpxPath, sillyTavernArgs, {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
detached: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.sillyTavernProcess.stdout) {
|
||||||
|
this.sillyTavernProcess.stdout.on('data', (data: Buffer) => {
|
||||||
|
const output = data.toString();
|
||||||
|
this.windowManager.sendKoboldOutput(output, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sillyTavernProcess.stderr) {
|
||||||
|
this.sillyTavernProcess.stderr.on('data', (data: Buffer) => {
|
||||||
|
const output = data.toString();
|
||||||
|
this.windowManager.sendKoboldOutput(output, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sillyTavernProcess.on(
|
||||||
|
'exit',
|
||||||
|
(code: number | null, signal: string | null) => {
|
||||||
|
const message = signal
|
||||||
|
? `SillyTavern terminated with signal ${signal}`
|
||||||
|
: `SillyTavern exited with code ${code}`;
|
||||||
|
this.windowManager.sendKoboldOutput(message);
|
||||||
|
this.sillyTavernProcess = null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sillyTavernProcess.on('error', (error) => {
|
||||||
|
this.logManager.logError('SillyTavern process error:', error);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`SillyTavern error: ${error.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sillyTavernProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.proxyPort) {
|
||||||
|
this.createProxyServer(config.port, config.proxyPort);
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`SillyTavern proxy starting at http://localhost:${config.proxyPort} (forwarding to port ${config.port})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.windowManager.sendKoboldOutput(
|
||||||
|
`SillyTavern starting at http://localhost:${config.port}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError(
|
||||||
|
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopFrontend(): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (this.sillyTavernProcess) {
|
||||||
|
promises.push(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.logManager.logError(
|
||||||
|
'SillyTavern did not close within timeout',
|
||||||
|
new Error('Process close timeout')
|
||||||
|
);
|
||||||
|
this.sillyTavernProcess = null;
|
||||||
|
resolve();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.sillyTavernProcess?.kill('SIGTERM');
|
||||||
|
this.sillyTavernProcess?.once('exit', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.sillyTavernProcess = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.proxyServer) {
|
||||||
|
promises.push(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
this.proxyServer?.close(() => {
|
||||||
|
this.proxyServer = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
if (this.sillyTavernProcess) {
|
||||||
|
try {
|
||||||
|
await this.stopFrontend();
|
||||||
|
} catch (error) {
|
||||||
|
this.logManager.logError(
|
||||||
|
'Error during SillyTavernManager cleanup:',
|
||||||
|
error as Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { BrowserWindow, app, Menu, shell, nativeImage } from 'electron';
|
import { BrowserWindow, app, Menu, shell, nativeImage } from 'electron';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { GITHUB_API } from '../../constants';
|
import { readFileSync } from 'fs';
|
||||||
|
import { stripVTControlCharacters } from 'util';
|
||||||
|
import { GITHUB_API, PRODUCT_NAME } from '../../constants';
|
||||||
|
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
private mainWindow: BrowserWindow | null = null;
|
private mainWindow: BrowserWindow | null = null;
|
||||||
|
|
@ -25,7 +28,7 @@ export class WindowManager {
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 600,
|
height: 600,
|
||||||
icon: iconImage,
|
icon: iconImage,
|
||||||
title: 'Friendly Kobold',
|
title: PRODUCT_NAME,
|
||||||
show: false,
|
show: false,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
|
@ -119,6 +122,23 @@ export class WindowManager {
|
||||||
return this.mainWindow;
|
return this.mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendToRenderer<T extends IPCChannel>(
|
||||||
|
channel: T,
|
||||||
|
...args: IPCChannelPayloads[T]
|
||||||
|
): void {
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send(channel, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendKoboldOutput(message: string, raw?: boolean): void {
|
||||||
|
const cleanMessage = stripVTControlCharacters(message);
|
||||||
|
this.sendToRenderer(
|
||||||
|
'kobold-output',
|
||||||
|
raw ? cleanMessage : `${cleanMessage}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public cleanup() {
|
public cleanup() {
|
||||||
if (this.mainWindow) {
|
if (this.mainWindow) {
|
||||||
this.mainWindow.removeAllListeners();
|
this.mainWindow.removeAllListeners();
|
||||||
|
|
@ -280,7 +300,7 @@ export class WindowManager {
|
||||||
|
|
||||||
private async showAboutDialog() {
|
private async showAboutDialog() {
|
||||||
const packagePath = join(app.getAppPath(), 'package.json');
|
const packagePath = join(app.getAppPath(), 'package.json');
|
||||||
const packageInfo = require(packagePath);
|
const packageInfo = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||||
const electronVersion = process.versions.electron;
|
const electronVersion = process.versions.electron;
|
||||||
const chromeVersion = process.versions.chrome;
|
const chromeVersion = process.versions.chrome;
|
||||||
const nodeVersion = process.versions.node;
|
const nodeVersion = process.versions.node;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { join, dirname } from 'path';
|
||||||
import { LogManager } from '@/main/managers/LogManager';
|
import { LogManager } from '@/main/managers/LogManager';
|
||||||
import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
|
||||||
import type { HardwareService } from '@/main/services/HardwareService';
|
import type { HardwareService } from '@/main/services/HardwareService';
|
||||||
|
import type { BackendOption } from '@/types';
|
||||||
|
|
||||||
export interface BackendSupport {
|
export interface BackendSupport {
|
||||||
rocm: boolean;
|
rocm: boolean;
|
||||||
|
|
@ -15,10 +16,7 @@ export interface BackendSupport {
|
||||||
|
|
||||||
export class BinaryService {
|
export class BinaryService {
|
||||||
private backendSupportCache = new Map<string, BackendSupport>();
|
private backendSupportCache = new Map<string, BackendSupport>();
|
||||||
private availableBackendsCache = new Map<
|
private availableBackendsCache = new Map<string, BackendOption[]>();
|
||||||
string,
|
|
||||||
Array<{ value: string; label: string; devices?: string[] }>
|
|
||||||
>();
|
|
||||||
private logManager: LogManager;
|
private logManager: LogManager;
|
||||||
private koboldManager: KoboldCppManager;
|
private koboldManager: KoboldCppManager;
|
||||||
private hardwareService: HardwareService;
|
private hardwareService: HardwareService;
|
||||||
|
|
@ -107,20 +105,25 @@ export class BinaryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableBackends(): Promise<
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
Array<{ value: string; label: string; devices?: string[] }>
|
async getAvailableBackends(
|
||||||
> {
|
includeDisabled = false
|
||||||
|
): Promise<BackendOption[]> {
|
||||||
try {
|
try {
|
||||||
const [currentBinaryInfo, hardwareCapabilities] = await Promise.all([
|
const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] =
|
||||||
this.koboldManager.getCurrentBinaryInfo(),
|
await Promise.all([
|
||||||
this.hardwareService.detectGPUCapabilities(),
|
this.koboldManager.getCurrentBinaryInfo(),
|
||||||
]);
|
this.hardwareService.detectGPUCapabilities(),
|
||||||
|
includeDisabled
|
||||||
|
? this.hardwareService.detectCPU()
|
||||||
|
: Promise.resolve(null),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!currentBinaryInfo?.path) {
|
if (!currentBinaryInfo?.path) {
|
||||||
return [{ value: 'cpu', label: 'CPU' }];
|
return [{ value: 'cpu', label: 'CPU' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `${currentBinaryInfo.path}:${JSON.stringify(hardwareCapabilities)}`;
|
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
|
||||||
|
|
||||||
if (this.availableBackendsCache.has(cacheKey)) {
|
if (this.availableBackendsCache.has(cacheKey)) {
|
||||||
return this.availableBackendsCache.get(cacheKey)!;
|
return this.availableBackendsCache.get(cacheKey)!;
|
||||||
|
|
@ -132,49 +135,70 @@ export class BinaryService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const backends: Array<{
|
const backends: BackendOption[] = [];
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
devices?: string[];
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
if (backendSupport.cuda && hardwareCapabilities.cuda.supported) {
|
if (backendSupport.cuda) {
|
||||||
backends.push({
|
const isSupported = hardwareCapabilities.cuda.supported;
|
||||||
value: 'cuda',
|
if (isSupported || includeDisabled) {
|
||||||
label: 'CUDA',
|
backends.push({
|
||||||
devices: hardwareCapabilities.cuda.devices,
|
value: 'cuda',
|
||||||
});
|
label: 'CUDA',
|
||||||
|
devices: hardwareCapabilities.cuda.devices,
|
||||||
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backendSupport.rocm && hardwareCapabilities.rocm.supported) {
|
if (backendSupport.rocm) {
|
||||||
backends.push({
|
const isSupported = hardwareCapabilities.rocm.supported;
|
||||||
value: 'rocm',
|
if (isSupported || includeDisabled) {
|
||||||
label: 'ROCm',
|
backends.push({
|
||||||
devices: hardwareCapabilities.rocm.devices,
|
value: 'rocm',
|
||||||
});
|
label: 'ROCm',
|
||||||
|
devices: hardwareCapabilities.rocm.devices,
|
||||||
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backendSupport.vulkan && hardwareCapabilities.vulkan.supported) {
|
if (backendSupport.vulkan) {
|
||||||
backends.push({
|
const isSupported = hardwareCapabilities.vulkan.supported;
|
||||||
value: 'vulkan',
|
if (isSupported || includeDisabled) {
|
||||||
label: 'Vulkan',
|
backends.push({
|
||||||
devices: hardwareCapabilities.vulkan.devices,
|
value: 'vulkan',
|
||||||
});
|
label: 'Vulkan',
|
||||||
|
devices: hardwareCapabilities.vulkan.devices,
|
||||||
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backendSupport.clblast && hardwareCapabilities.clblast.supported) {
|
if (backendSupport.clblast) {
|
||||||
backends.push({
|
const isSupported = hardwareCapabilities.clblast.supported;
|
||||||
value: 'clblast',
|
if (isSupported || includeDisabled) {
|
||||||
label: 'CLBlast',
|
backends.push({
|
||||||
devices: hardwareCapabilities.clblast.devices,
|
value: 'clblast',
|
||||||
});
|
label: 'CLBlast',
|
||||||
|
devices: hardwareCapabilities.clblast.devices,
|
||||||
|
disabled: includeDisabled ? !isSupported : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backends.push({
|
backends.push({
|
||||||
value: 'cpu',
|
value: 'cpu',
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
|
devices: cpuCapabilities?.devices,
|
||||||
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (includeDisabled) {
|
||||||
|
backends.sort((a, b) => {
|
||||||
|
if (a.disabled === b.disabled) return 0;
|
||||||
|
return a.disabled ? 1 : -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.availableBackendsCache.set(cacheKey, backends);
|
this.availableBackendsCache.set(cacheKey, backends);
|
||||||
return backends;
|
return backends;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-comments/disallowComments */
|
/* eslint-disable no-comments/disallowComments */
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import { shortenDeviceName } from '@/utils';
|
import { shortenDeviceName } from '@/utils/server';
|
||||||
import { LogManager } from '@/main/managers/LogManager';
|
import { LogManager } from '@/main/managers/LogManager';
|
||||||
import type {
|
import type {
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import { existsSync, readFileSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { homedir } from 'os';
|
|
||||||
|
|
||||||
import { CONFIG_FILE_NAME } from '@/constants';
|
|
||||||
|
|
||||||
export class CliHandler {
|
|
||||||
private getConfigPath(): string {
|
|
||||||
const platform = process.platform;
|
|
||||||
const home = homedir();
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case 'win32':
|
|
||||||
return join(
|
|
||||||
home,
|
|
||||||
'AppData',
|
|
||||||
'Roaming',
|
|
||||||
'Friendly Kobold',
|
|
||||||
CONFIG_FILE_NAME
|
|
||||||
);
|
|
||||||
case 'darwin':
|
|
||||||
return join(
|
|
||||||
home,
|
|
||||||
'Library',
|
|
||||||
'Application Support',
|
|
||||||
'Friendly Kobold',
|
|
||||||
CONFIG_FILE_NAME
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return join(home, '.config', 'Friendly Kobold', CONFIG_FILE_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCurrentKoboldBinary(): string | null {
|
|
||||||
try {
|
|
||||||
const configPath = this.getConfigPath();
|
|
||||||
if (!existsSync(configPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
||||||
return config.currentKoboldBinary || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleCliMode(args: string[]): Promise<void> {
|
|
||||||
const currentBinary = this.getCurrentKoboldBinary();
|
|
||||||
|
|
||||||
if (!currentBinary) {
|
|
||||||
console.error(
|
|
||||||
'Error: No KoboldCpp binary found. Please run the GUI first to download KoboldCpp.'
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(currentBinary)) {
|
|
||||||
console.error(`Error: KoboldCpp binary not found at: ${currentBinary}`);
|
|
||||||
console.error('Please run the GUI to download or reconfigure KoboldCpp.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const isWindows = process.platform === 'win32';
|
|
||||||
|
|
||||||
const child = spawn(currentBinary, args, {
|
|
||||||
stdio: isWindows ? 'pipe' : 'inherit',
|
|
||||||
detached: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isWindows) {
|
|
||||||
child.stdout?.setEncoding('utf8');
|
|
||||||
child.stderr?.setEncoding('utf8');
|
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
|
||||||
process.stdout.write(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
|
||||||
process.stderr.write(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (child.stdin && process.stdin.readable) {
|
|
||||||
process.stdin.pipe(child.stdin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
|
||||||
if (signal) {
|
|
||||||
console.log(`\nProcess terminated with signal: ${signal}`);
|
|
||||||
process.exit(128 + (signal === 'SIGTERM' ? 15 : 2));
|
|
||||||
} else if (code !== null) {
|
|
||||||
process.exit(code);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (error) => {
|
|
||||||
console.error(`Failed to start KoboldCpp: ${error.message}`);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSignal = () => {
|
|
||||||
console.log('\nReceived termination signal, terminating KoboldCpp...');
|
|
||||||
if (!child.killed) {
|
|
||||||
child.kill('SIGTERM');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('SIGINT', handleSignal);
|
|
||||||
process.on('SIGTERM', handleSignal);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseArguments(argv: string[]): {
|
|
||||||
isCliMode: boolean;
|
|
||||||
args: string[];
|
|
||||||
} {
|
|
||||||
const cliIndex = argv.indexOf('--cli');
|
|
||||||
|
|
||||||
if (cliIndex === -1) {
|
|
||||||
return { isCliMode: false, args: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const koboldArgs = argv.slice(cliIndex + 1);
|
|
||||||
return { isCliMode: true, args: koboldArgs };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,8 @@ const koboldAPI: KoboldAPI = {
|
||||||
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
|
||||||
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
||||||
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
|
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
|
||||||
getAvailableBackends: () => ipcRenderer.invoke('kobold:getAvailableBackends'),
|
getAvailableBackends: (includeDisabled?: boolean) =>
|
||||||
|
ipcRenderer.invoke('kobold:getAvailableBackends', includeDisabled),
|
||||||
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
|
||||||
selectInstallDirectory: () =>
|
selectInstallDirectory: () =>
|
||||||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
@import '@mantine/core/styles.css';
|
@import '@mantine/core/styles.css';
|
||||||
|
|
||||||
|
/* TODO: something (mantine?) is setting this sometimes to "none" on the terminal tab */
|
||||||
|
body {
|
||||||
|
user-select: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom scrollbars */
|
/* Custom scrollbars */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
|
|
|
||||||
18
src/types/electron.d.ts
vendored
18
src/types/electron.d.ts
vendored
|
|
@ -2,12 +2,12 @@ import type {
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
GPUCapabilities,
|
GPUCapabilities,
|
||||||
BasicGPUInfo,
|
BasicGPUInfo,
|
||||||
HardwareInfo,
|
|
||||||
PlatformInfo,
|
PlatformInfo,
|
||||||
GPUMemoryInfo,
|
GPUMemoryInfo,
|
||||||
} from '@/types/hardware';
|
} from '@/types/hardware';
|
||||||
|
import type { BackendOption } from '@/types';
|
||||||
|
|
||||||
interface GitHubAsset {
|
export interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
browser_download_url: string;
|
browser_download_url: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|
@ -32,13 +32,13 @@ export interface UpdateInfo {
|
||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReleaseWithStatus {
|
export interface ReleaseWithStatus {
|
||||||
release: GitHubRelease;
|
release: GitHubRelease;
|
||||||
availableAssets: Array<{
|
availableAssets: {
|
||||||
asset: GitHubAsset;
|
asset: GitHubAsset;
|
||||||
isDownloaded: boolean;
|
isDownloaded: boolean;
|
||||||
installedVersion?: string;
|
installedVersion?: string;
|
||||||
}>;
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstalledVersion {
|
export interface InstalledVersion {
|
||||||
|
|
@ -79,9 +79,7 @@ export interface KoboldAPI {
|
||||||
failsafe: boolean;
|
failsafe: boolean;
|
||||||
cuda: boolean;
|
cuda: boolean;
|
||||||
} | null>;
|
} | null>;
|
||||||
getAvailableBackends: () => Promise<
|
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>;
|
||||||
Array<{ value: string; label: string; devices?: string[] }>
|
|
||||||
>;
|
|
||||||
getCurrentInstallDir: () => Promise<string>;
|
getCurrentInstallDir: () => Promise<string>;
|
||||||
selectInstallDirectory: () => Promise<string | null>;
|
selectInstallDirectory: () => Promise<string | null>;
|
||||||
downloadRelease: (
|
downloadRelease: (
|
||||||
|
|
@ -97,9 +95,7 @@ export interface KoboldAPI {
|
||||||
launchKoboldCpp: (
|
launchKoboldCpp: (
|
||||||
args?: string[]
|
args?: string[]
|
||||||
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
||||||
getConfigFiles: () => Promise<
|
getConfigFiles: () => Promise<{ name: string; path: string; size: number }[]>;
|
||||||
Array<{ name: string; path: string; size: number }>
|
|
||||||
>;
|
|
||||||
saveConfigFile: (
|
saveConfigFile: (
|
||||||
configName: string,
|
configName: string,
|
||||||
configData: {
|
configData: {
|
||||||
|
|
|
||||||
21
src/types/index.d.ts
vendored
21
src/types/index.d.ts
vendored
|
|
@ -8,8 +8,12 @@ export type InterfaceTab = 'terminal' | 'chat';
|
||||||
|
|
||||||
export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
|
export type SdConvDirectMode = 'off' | 'vaeonly' | 'full';
|
||||||
|
|
||||||
|
export type FrontendPreference = 'koboldcpp' | 'sillytavern';
|
||||||
|
|
||||||
export type ServerTabMode = 'chat' | 'image-generation';
|
export type ServerTabMode = 'chat' | 'image-generation';
|
||||||
|
|
||||||
|
export type Screen = 'welcome' | 'download' | 'launch' | 'interface';
|
||||||
|
|
||||||
export interface GitHubAsset {
|
export interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
browser_download_url: string;
|
browser_download_url: string;
|
||||||
|
|
@ -39,11 +43,12 @@ export interface InstalledVersion {
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export interface SelectOption {
|
||||||
CPUCapabilities,
|
value: string;
|
||||||
GPUCapabilities,
|
label: string;
|
||||||
BasicGPUInfo,
|
}
|
||||||
HardwareInfo,
|
|
||||||
PlatformInfo,
|
export interface BackendOption extends SelectOption {
|
||||||
SystemCapabilities,
|
devices?: string[];
|
||||||
} from '@/types/hardware';
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
12
src/types/ipc.d.ts
vendored
Normal file
12
src/types/ipc.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type IPCChannel =
|
||||||
|
| 'download-progress'
|
||||||
|
| 'install-dir-changed'
|
||||||
|
| 'versions-updated'
|
||||||
|
| 'kobold-output';
|
||||||
|
|
||||||
|
export interface IPCChannelPayloads {
|
||||||
|
'download-progress': [progress: number];
|
||||||
|
'install-dir-changed': [newPath: string];
|
||||||
|
'versions-updated': [];
|
||||||
|
'kobold-output': [message: string];
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
export function parseCLBlastDevice(deviceString: string): {
|
|
||||||
deviceIndex: number;
|
|
||||||
platformIndex: number;
|
|
||||||
} | null {
|
|
||||||
const match = deviceString.match(/\[(\d+),(\d+)\]$/);
|
|
||||||
if (match) {
|
|
||||||
return {
|
|
||||||
deviceIndex: parseInt(match[1], 10),
|
|
||||||
platformIndex: parseInt(match[2], 10),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { KOBOLDAI_URLS } from '@/constants';
|
import { ROCM } from '@/constants';
|
||||||
|
|
||||||
export const formatDownloadSize = (size: number, url?: string): string => {
|
export const formatDownloadSize = (size: number, url?: string): string => {
|
||||||
if (!size) return '';
|
if (!size) return '';
|
||||||
|
|
||||||
const isApproximateSize = url?.includes(KOBOLDAI_URLS.DOMAIN);
|
const isApproximateSize = url?.includes(ROCM.DOWNLOAD_URL);
|
||||||
|
|
||||||
return isApproximateSize
|
return isApproximateSize
|
||||||
? `~${formatFileSizeInMB(size)}`
|
? `~${formatFileSizeInMB(size)}`
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
export * from './assets';
|
export * from './assets';
|
||||||
export * from './clblast';
|
|
||||||
export * from './downloadUtils';
|
export * from './downloadUtils';
|
||||||
export * from './hardware';
|
|
||||||
export * from './imageModelPresets';
|
export * from './imageModelPresets';
|
||||||
|
export * from './linkifyTerminal';
|
||||||
export * from './platform';
|
export * from './platform';
|
||||||
export * from './sounds';
|
export * from './sounds';
|
||||||
export * from './terminal';
|
export * from './terminal';
|
||||||
|
|
|
||||||
26
src/utils/linkifyTerminal.ts
Normal file
26
src/utils/linkifyTerminal.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;
|
||||||
|
|
||||||
|
export const linkifyText = (text: string): string =>
|
||||||
|
text.replace(URL_REGEX, (url) => {
|
||||||
|
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
|
||||||
|
const trailingPunctuation = url.slice(cleanUrl.length);
|
||||||
|
|
||||||
|
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #339af0; text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const escapeHtmlExceptLinks = (text: string): string => {
|
||||||
|
const escaped = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
return linkifyText(escaped);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processTerminalContent = (content: string): string => {
|
||||||
|
if (!content) return '';
|
||||||
|
|
||||||
|
return escapeHtmlExceptLinks(content);
|
||||||
|
};
|
||||||
2
src/utils/server/index.ts
Normal file
2
src/utils/server/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './hardware';
|
||||||
|
export * from './paths';
|
||||||
12
src/utils/server/paths.ts
Normal file
12
src/utils/server/paths.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function getPlatformPathFromWindowsPath(
|
||||||
|
basePath: string,
|
||||||
|
windowsBinaryPath: string
|
||||||
|
): string {
|
||||||
|
const binaryName =
|
||||||
|
process.platform === 'win32'
|
||||||
|
? windowsBinaryPath
|
||||||
|
: windowsBinaryPath.replace(/\.[^.]*$/, '');
|
||||||
|
return join(basePath, 'node_modules', '.bin', binaryName);
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
import stripAnsi from 'strip-ansi';
|
|
||||||
|
|
||||||
export const handleTerminalOutput = (
|
export const handleTerminalOutput = (
|
||||||
prevContent: string,
|
prevContent: string,
|
||||||
newData: string
|
newData: string
|
||||||
): string => {
|
): string => {
|
||||||
try {
|
try {
|
||||||
const cleanData = stripAnsi(newData);
|
if (newData.includes('\r') && !newData.includes('\r\n')) {
|
||||||
|
const lines = (prevContent + newData).split('\n');
|
||||||
|
|
||||||
if (cleanData.includes('\r') && !cleanData.includes('\r\n')) {
|
|
||||||
const lines = (prevContent + cleanData).split('\n');
|
|
||||||
return lines
|
return lines
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
if (line.includes('\r')) {
|
if (line.includes('\r')) {
|
||||||
const parts = line.split('\r');
|
const parts = line.split('\r');
|
||||||
return parts[parts.length - 1];
|
return parts[parts.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
return line;
|
return line;
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return prevContent + cleanData;
|
return prevContent + newData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.electronAPI.logs.logError('Terminal Basic Error', error as Error);
|
window.electronAPI.logs.logError('Terminal Basic Error', error as Error);
|
||||||
return prevContent + newData;
|
return prevContent + newData;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export const isValidUrl = (string: string): boolean => {
|
const isValidUrl = (string: string): boolean => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(string);
|
const url = new URL(string);
|
||||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
|
@ -7,7 +7,7 @@ export const isValidUrl = (string: string): boolean => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidFilePath = (path: string): boolean => {
|
const isValidFilePath = (path: string): boolean => {
|
||||||
if (!path.trim()) return false;
|
if (!path.trim()) return false;
|
||||||
|
|
||||||
const validExtensions = ['.gguf'];
|
const validExtensions = ['.gguf'];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue