mirror of
https://github.com/open-webui/desktop
synced 2025-06-26 18:15:59 +00:00
chore: format
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/electron",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/electron",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
|
||||
"tabWidth": 4
|
||||
}
|
||||
10
LICENSE
10
LICENSE
@@ -5,15 +5,15 @@ Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Open WebUI App (Experimental) 🚀
|
||||
|
||||
**Open WebUI App** is the upcoming cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It provides a streamlined, all-in-one experience for installing and managing Open WebUI directly on your personal computer or connecting to remote Open WebUI servers.
|
||||
**Open WebUI App** is the upcoming cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It provides a streamlined, all-in-one experience for installing and managing Open WebUI directly on your personal computer or connecting to remote Open WebUI servers.
|
||||
|
||||
This project is still **super experimental** and under active development. 🛠️ Expect rapid iteration and potential breaking changes during this phase.
|
||||
|
||||
@@ -25,4 +25,4 @@ This project is licensed under the [BSD-3-Clause License](LICENSE).
|
||||
|
||||
We're actively developing Open WebUI App. Follow [Open WebUI](https://github.com/open-webui/open-webui) for updates, and join the [community on Discord](https://discord.gg/5rJgQTnV4s) to stay involved.
|
||||
|
||||
Let's build something amazing together! 💪
|
||||
Let's build something amazing together! 💪
|
||||
|
||||
34
dev.md
34
dev.md
@@ -15,27 +15,31 @@ Ensure these tools are properly installed and configured before proceeding.
|
||||
## Getting Started
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone https://github.com/open-webui/app
|
||||
cd app
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/open-webui/app
|
||||
cd app
|
||||
```
|
||||
|
||||
2. **Install Node.js dependencies**:
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
3. **Generate the Python environment tarball**:
|
||||
```bash
|
||||
npm run create:python-tar
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run create:python-tar
|
||||
```
|
||||
|
||||
4. **Start the development environment**:
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
This will launch the project in development mode.
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
This will launch the project in development mode.
|
||||
|
||||
---
|
||||
|
||||
@@ -56,4 +60,4 @@ This will create the necessary files for distribution in the `out` directory.
|
||||
- Make sure you have the required versions of **conda**, **conda-pack**, and **conda-lock** to avoid compatibility issues.
|
||||
- If you encounter any issues, check the project-specific scripts in the `package.json` file.
|
||||
|
||||
Enjoy developing! 🚀
|
||||
Enjoy developing! 🚀
|
||||
|
||||
112
forge.config.ts
112
forge.config.ts
@@ -1,61 +1,61 @@
|
||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
||||
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||
import { MakerDeb } from "@electron-forge/maker-deb";
|
||||
import { MakerRpm } from "@electron-forge/maker-rpm";
|
||||
import { VitePlugin } from "@electron-forge/plugin-vite";
|
||||
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
||||
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
||||
import type { ForgeConfig } from '@electron-forge/shared-types';
|
||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
||||
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
||||
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
icon: "public/assets/icon.png",
|
||||
extraResource: ["public/assets", "resources"],
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({}),
|
||||
new MakerZIP({}, ["darwin"]),
|
||||
new MakerRpm({}),
|
||||
new MakerDeb({}),
|
||||
],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
// If you are familiar with Vite configuration, it will look really familiar.
|
||||
build: [
|
||||
{
|
||||
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
|
||||
entry: "src/main.ts",
|
||||
config: "vite.main.config.ts",
|
||||
target: "main",
|
||||
},
|
||||
{
|
||||
entry: "src/preload.ts",
|
||||
config: "vite.preload.config.ts",
|
||||
target: "preload",
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
name: "main_window",
|
||||
config: "vite.renderer.config.mts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
icon: 'public/assets/icon.png',
|
||||
extraResource: ['public/assets', 'resources']
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({}),
|
||||
new MakerZIP({}, ['darwin']),
|
||||
new MakerRpm({}),
|
||||
new MakerDeb({})
|
||||
],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
// If you are familiar with Vite configuration, it will look really familiar.
|
||||
build: [
|
||||
{
|
||||
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
|
||||
entry: 'src/main.ts',
|
||||
config: 'vite.main.config.ts',
|
||||
target: 'main'
|
||||
},
|
||||
{
|
||||
entry: 'src/preload.ts',
|
||||
config: 'vite.preload.config.ts',
|
||||
target: 'preload'
|
||||
}
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
name: 'main_window',
|
||||
config: 'vite.renderer.config.mts'
|
||||
}
|
||||
]
|
||||
}),
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
18
index.html
18
index.html
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Open WebUI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/renderer.ts"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Open WebUI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/renderer.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
23450
package-lock.json
generated
23450
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
108
package.json
108
package.json
@@ -1,53 +1,59 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"productName": "Open WebUI",
|
||||
"version": "0.0.1",
|
||||
"description": "Open WebUI",
|
||||
"main": ".vite/build/main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "eslint --ext .ts,.tsx .",
|
||||
"create:conda-lock": "cd resources && rimraf conda-lock.yml && conda-lock -f environment.yml && cd -",
|
||||
"create:python-tar": "rimraf ./resources/python.tar.gz && conda-lock install --prefix ./resources/python ./resources/conda-lock.yml && conda pack -p ./resources/python -o ./resources/python.tar.gz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
"@electron-forge/maker-deb": "^7.6.0",
|
||||
"@electron-forge/maker-rpm": "^7.6.0",
|
||||
"@electron-forge/maker-squirrel": "^7.6.0",
|
||||
"@electron-forge/maker-zip": "^7.6.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.0",
|
||||
"@electron-forge/plugin-fuses": "^7.6.0",
|
||||
"@electron-forge/plugin-vite": "^7.6.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.0.0-beta.8",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"electron": "33.3.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"svelte": "^5.17.1",
|
||||
"svelte-check": "^4.1.3",
|
||||
"tailwindcss": "^4.0.0-beta.8",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.7"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "Timothy Jaeryang Baek",
|
||||
"email": "tim@openwebui.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"tar": "^7.4.3",
|
||||
"update-electron-app": "^3.1.0"
|
||||
}
|
||||
"name": "open-webui",
|
||||
"productName": "Open WebUI",
|
||||
"version": "0.0.1",
|
||||
"description": "Open WebUI",
|
||||
"main": ".vite/build/main.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "eslint --ext .ts,.tsx .",
|
||||
"format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"",
|
||||
"create:conda-lock": "cd resources && rimraf conda-lock.yml && conda-lock -f environment.yml && cd -",
|
||||
"create:python-tar": "rimraf ./resources/python.tar.gz && conda-lock install --prefix ./resources/python ./resources/conda-lock.yml && conda pack -p ./resources/python -o ./resources/python.tar.gz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
"@electron-forge/maker-deb": "^7.6.0",
|
||||
"@electron-forge/maker-rpm": "^7.6.0",
|
||||
"@electron-forge/maker-squirrel": "^7.6.0",
|
||||
"@electron-forge/maker-zip": "^7.6.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.0",
|
||||
"@electron-forge/plugin-fuses": "^7.6.0",
|
||||
"@electron-forge/plugin-vite": "^7.6.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.0.0-beta.8",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"dompurify": "^3.2.3",
|
||||
"electron": "33.3.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"marked": "^15.0.6",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"svelte": "^5.17.1",
|
||||
"svelte-check": "^4.1.3",
|
||||
"tailwindcss": "^4.0.0-beta.8",
|
||||
"tippy.js": "^6.3.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.7"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "Timothy Jaeryang Baek",
|
||||
"email": "tim@openwebui.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"tar": "^7.4.3",
|
||||
"update-electron-app": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
359
src/main.ts
359
src/main.ts
@@ -1,219 +1,220 @@
|
||||
import {
|
||||
app,
|
||||
nativeImage,
|
||||
Tray,
|
||||
Menu,
|
||||
MenuItem,
|
||||
BrowserWindow,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
} from "electron";
|
||||
import path from "path";
|
||||
import started from "electron-squirrel-startup";
|
||||
app,
|
||||
nativeImage,
|
||||
Tray,
|
||||
Menu,
|
||||
MenuItem,
|
||||
BrowserWindow,
|
||||
globalShortcut,
|
||||
ipcMain
|
||||
} from 'electron';
|
||||
import path from 'path';
|
||||
import started from 'electron-squirrel-startup';
|
||||
|
||||
import {
|
||||
installPackage,
|
||||
removePackage,
|
||||
startServer,
|
||||
stopAllServers,
|
||||
validateInstallation,
|
||||
} from "./utils";
|
||||
installPackage,
|
||||
removePackage,
|
||||
startServer,
|
||||
stopAllServers,
|
||||
validateInstallation
|
||||
} from './utils';
|
||||
|
||||
// Restrict app to a single instance
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit(); // Quit if another instance is already running
|
||||
app.quit(); // Quit if another instance is already running
|
||||
} else {
|
||||
// Handle second-instance logic
|
||||
app.on("second-instance", (event, argv, workingDirectory) => {
|
||||
// This event happens if a second instance is launched
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore(); // Restore if minimized
|
||||
mainWindow.show(); // Show existing window
|
||||
mainWindow.focus(); // Focus the existing window
|
||||
}
|
||||
});
|
||||
// Handle second-instance logic
|
||||
app.on('second-instance', (event, argv, workingDirectory) => {
|
||||
// This event happens if a second instance is launched
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore(); // Restore if minimized
|
||||
mainWindow.show(); // Show existing window
|
||||
mainWindow.focus(); // Focus the existing window
|
||||
}
|
||||
});
|
||||
|
||||
// Handle creating/removing shortcuts on Windows during installation/uninstallation
|
||||
if (started) {
|
||||
app.quit();
|
||||
}
|
||||
// Handle creating/removing shortcuts on Windows during installation/uninstallation
|
||||
if (started) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: "Open WebUI",
|
||||
iconPath: path.join(__dirname, "assets/icon.png"),
|
||||
applicationVersion: app.getVersion(),
|
||||
version: app.getVersion(),
|
||||
website: "https://openwebui.com",
|
||||
copyright: `© ${new Date().getFullYear()} Open WebUI (Timothy Jaeryang Baek)`,
|
||||
});
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: 'Open WebUI',
|
||||
iconPath: path.join(__dirname, 'assets/icon.png'),
|
||||
applicationVersion: app.getVersion(),
|
||||
version: app.getVersion(),
|
||||
website: 'https://openwebui.com',
|
||||
copyright: `© ${new Date().getFullYear()} Open WebUI (Timothy Jaeryang Baek)`
|
||||
});
|
||||
|
||||
// Main application logic
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
// Main application logic
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
|
||||
const loadDefaultView = () => {
|
||||
// Load index.html or dev server URL
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
|
||||
);
|
||||
}
|
||||
};
|
||||
const loadDefaultView = () => {
|
||||
// Load index.html or dev server URL
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onReady = async () => {
|
||||
console.log(process.resourcesPath);
|
||||
console.log(app.getName());
|
||||
console.log(app.getPath("userData"));
|
||||
console.log(app.getPath("appData"));
|
||||
const onReady = async () => {
|
||||
console.log(process.resourcesPath);
|
||||
console.log(app.getName());
|
||||
console.log(app.getPath('userData'));
|
||||
console.log(app.getPath('appData'));
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
icon: path.join(__dirname, "assets/icon.png"),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
titleBarStyle: "hidden",
|
||||
});
|
||||
mainWindow.setIcon(path.join(__dirname, "assets/icon.png"));
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
icon: path.join(__dirname, 'assets/icon.png'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
titleBarStyle: 'hidden',
|
||||
trafficLightPosition: { x: 10, y: 10 },
|
||||
// expose window controlls in Windows/Linux
|
||||
...(process.platform !== 'darwin' ? { titleBarOverlay: true } : {})
|
||||
});
|
||||
mainWindow.setIcon(path.join(__dirname, 'assets/icon.png'));
|
||||
|
||||
loadDefaultView();
|
||||
if (!app.isPackaged) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
loadDefaultView();
|
||||
if (!app.isPackaged) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
if (validateInstallation()) {
|
||||
const serverUrl = await startServer();
|
||||
mainWindow.loadURL(serverUrl);
|
||||
}
|
||||
if (validateInstallation()) {
|
||||
const serverUrl = await startServer();
|
||||
mainWindow.loadURL(serverUrl);
|
||||
}
|
||||
|
||||
globalShortcut.register("Alt+CommandOrControl+O", () => {
|
||||
mainWindow?.show();
|
||||
globalShortcut.register('Alt+CommandOrControl+O', () => {
|
||||
mainWindow?.show();
|
||||
|
||||
if (mainWindow?.isMinimized()) mainWindow?.restore();
|
||||
mainWindow?.focus();
|
||||
});
|
||||
if (mainWindow?.isMinimized()) mainWindow?.restore();
|
||||
mainWindow?.focus();
|
||||
});
|
||||
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
const defaultMenu = Menu.getApplicationMenu();
|
||||
|
||||
console.log(defaultMenu);
|
||||
// Convert the default menu to a template we can modify
|
||||
let menuTemplate = defaultMenu ? defaultMenu.items.map((item) => item) : [];
|
||||
console.log(defaultMenu);
|
||||
// Convert the default menu to a template we can modify
|
||||
let menuTemplate = defaultMenu ? defaultMenu.items.map((item) => item) : [];
|
||||
|
||||
// Add your own custom menu items
|
||||
menuTemplate.push({
|
||||
label: "Action",
|
||||
submenu: [
|
||||
{
|
||||
label: "Home",
|
||||
accelerator: process.platform === "darwin" ? "Cmd+H" : "Ctrl+H",
|
||||
click: () => {
|
||||
loadDefaultView();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Add your own custom menu items
|
||||
menuTemplate.push({
|
||||
label: 'Action',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Home',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+H' : 'Ctrl+H',
|
||||
click: () => {
|
||||
loadDefaultView();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Build the updated menu and set it as the application menu
|
||||
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(updatedMenu);
|
||||
// Build the updated menu and set it as the application menu
|
||||
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(updatedMenu);
|
||||
|
||||
// Create a system tray icon
|
||||
const image = nativeImage.createFromPath(
|
||||
path.join(__dirname, "assets/tray.png")
|
||||
);
|
||||
tray = new Tray(image.resize({ width: 16, height: 16 }));
|
||||
// Create a system tray icon
|
||||
const image = nativeImage.createFromPath(path.join(__dirname, 'assets/tray.png'));
|
||||
tray = new Tray(image.resize({ width: 16, height: 16 }));
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show Application",
|
||||
click: () => {
|
||||
mainWindow.show(); // Show the main window when clicked
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Quit Open WebUI",
|
||||
accelerator: "CommandOrControl+Q",
|
||||
click: () => {
|
||||
app.isQuiting = true; // Mark as quitting
|
||||
app.quit(); // Quit the application
|
||||
},
|
||||
},
|
||||
]);
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show Application',
|
||||
click: () => {
|
||||
mainWindow.show(); // Show the main window when clicked
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Quit Open WebUI',
|
||||
accelerator: 'CommandOrControl+Q',
|
||||
click: () => {
|
||||
app.isQuiting = true; // Mark as quitting
|
||||
app.quit(); // Quit the application
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray.setToolTip("Open WebUI");
|
||||
tray.setContextMenu(trayMenu);
|
||||
tray.setToolTip('Open WebUI');
|
||||
tray.setContextMenu(trayMenu);
|
||||
|
||||
// Handle the close event
|
||||
mainWindow.on("close", (event) => {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault(); // Prevent the default close behavior
|
||||
mainWindow.hide(); // Hide the window instead of closing it
|
||||
}
|
||||
});
|
||||
};
|
||||
// Handle the close event
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault(); // Prevent the default close behavior
|
||||
mainWindow.hide(); // Hide the window instead of closing it
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ipcMain.handle("install", async (event) => {
|
||||
console.log("Installing package...");
|
||||
installPackage();
|
||||
});
|
||||
ipcMain.handle('install', async (event) => {
|
||||
console.log('Installing package...');
|
||||
installPackage();
|
||||
});
|
||||
|
||||
ipcMain.handle("remove", async (event) => {
|
||||
console.log("Resetting package...");
|
||||
removePackage();
|
||||
});
|
||||
ipcMain.handle('remove', async (event) => {
|
||||
console.log('Resetting package...');
|
||||
removePackage();
|
||||
});
|
||||
|
||||
ipcMain.handle("server:start", async (event) => {
|
||||
console.log("Starting server...");
|
||||
ipcMain.handle('server:start', async (event) => {
|
||||
console.log('Starting server...');
|
||||
|
||||
startServer();
|
||||
});
|
||||
startServer();
|
||||
});
|
||||
|
||||
ipcMain.handle("server:stop", async (event) => {
|
||||
console.log("Stopping server...");
|
||||
ipcMain.handle('server:stop', async (event) => {
|
||||
console.log('Stopping server...');
|
||||
|
||||
stopAllServers();
|
||||
});
|
||||
stopAllServers();
|
||||
});
|
||||
|
||||
ipcMain.handle("load-webui", async (event, arg) => {
|
||||
console.log(arg); // prints "ping"
|
||||
mainWindow.loadURL("http://localhost:8080");
|
||||
ipcMain.handle('load-webui', async (event, arg) => {
|
||||
console.log(arg); // prints "ping"
|
||||
mainWindow.loadURL('http://localhost:8080');
|
||||
|
||||
mainWindow.webContents.once("did-finish-load", () => {
|
||||
mainWindow.webContents.send("main:data", {
|
||||
type: "ping", // This is the same type you're listening for in the renderer
|
||||
});
|
||||
});
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
mainWindow.webContents.send('main:data', {
|
||||
type: 'ping' // This is the same type you're listening for in the renderer
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("send-ping", (event) => {
|
||||
console.log("Received PING from renderer process");
|
||||
mainWindow.webContents.send("ping-reply", "PONG from Main Process!");
|
||||
});
|
||||
});
|
||||
ipcMain.on('send-ping', (event) => {
|
||||
console.log('Received PING from renderer process');
|
||||
mainWindow.webContents.send('ping-reply', 'PONG from Main Process!');
|
||||
});
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
app.isQuiting = true; // Ensure quit flag is set
|
||||
stopAllServers();
|
||||
});
|
||||
app.on('before-quit', () => {
|
||||
app.isQuiting = true; // Ensure quit flag is set
|
||||
stopAllServers();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
// Quit when all windows are closed, except on macOS
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
onReady();
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
}
|
||||
});
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
onReady();
|
||||
} else {
|
||||
mainWindow?.show();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("ready", onReady);
|
||||
app.on('ready', onReady);
|
||||
}
|
||||
|
||||
118
src/preload.ts
118
src/preload.ts
@@ -1,75 +1,75 @@
|
||||
import { ipcRenderer, contextBridge } from "electron";
|
||||
import { ipcRenderer, contextBridge } from 'electron';
|
||||
|
||||
const isLocalSource = () => {
|
||||
// Check if the execution environment is local
|
||||
const origin = window.location.origin;
|
||||
// Check if the execution environment is local
|
||||
const origin = window.location.origin;
|
||||
|
||||
// Allow local sources: file protocol, localhost, or 0.0.0.0
|
||||
return (
|
||||
origin.startsWith("file://") ||
|
||||
origin.includes("localhost") ||
|
||||
origin.includes("127.0.0.1") ||
|
||||
origin.includes("0.0.0.0")
|
||||
);
|
||||
// Allow local sources: file protocol, localhost, or 0.0.0.0
|
||||
return (
|
||||
origin.startsWith('file://') ||
|
||||
origin.includes('localhost') ||
|
||||
origin.includes('127.0.0.1') ||
|
||||
origin.includes('0.0.0.0')
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
// Listen for messages from the main process
|
||||
ipcRenderer.on("main:data", (event, data) => {
|
||||
// Forward the message to the renderer using window.postMessage
|
||||
window.postMessage(
|
||||
{
|
||||
type: `electron:${data.type}`,
|
||||
data: data,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
});
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Listen for messages from the main process
|
||||
ipcRenderer.on('main:data', (event, data) => {
|
||||
// Forward the message to the renderer using window.postMessage
|
||||
window.postMessage(
|
||||
{
|
||||
type: `electron:${data.type}`,
|
||||
data: data
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
sendPing: async () => {
|
||||
console.log("Sending PING to main process...");
|
||||
await ipcRenderer.invoke("send-ping"); // Send the ping back to the main process
|
||||
},
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
sendPing: async () => {
|
||||
console.log('Sending PING to main process...');
|
||||
await ipcRenderer.invoke('send-ping'); // Send the ping back to the main process
|
||||
},
|
||||
|
||||
installPackage: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
installPackage: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
'Access restricted: This operation is only allowed in a local environment.'
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("install");
|
||||
},
|
||||
await ipcRenderer.invoke('install');
|
||||
},
|
||||
|
||||
removePackage: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
removePackage: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
'Access restricted: This operation is only allowed in a local environment.'
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("remove");
|
||||
},
|
||||
await ipcRenderer.invoke('remove');
|
||||
},
|
||||
|
||||
startServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
startServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
'Access restricted: This operation is only allowed in a local environment.'
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("server:start");
|
||||
},
|
||||
await ipcRenderer.invoke('server:start');
|
||||
},
|
||||
|
||||
stopServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
"Access restricted: This operation is only allowed in a local environment."
|
||||
);
|
||||
}
|
||||
stopServer: async () => {
|
||||
if (!isLocalSource()) {
|
||||
throw new Error(
|
||||
'Access restricted: This operation is only allowed in a local environment.'
|
||||
);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("server:stop");
|
||||
},
|
||||
await ipcRenderer.invoke('server:stop');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Main from "./lib/components/Main.svelte";
|
||||
onMount(() => {});
|
||||
import { onMount } from 'svelte';
|
||||
import Main from './lib/components/Main.svelte';
|
||||
onMount(() => {});
|
||||
</script>
|
||||
|
||||
<main class="w-screen h-screen bg-gray-900">
|
||||
<Main />
|
||||
<Main />
|
||||
</main>
|
||||
|
||||
@@ -1,39 +1,44 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
src: url("/assets/fonts/Archivo-Variable.ttf");
|
||||
font-display: swap;
|
||||
font-family: 'Archivo';
|
||||
src: url('/assets/fonts/Archivo-Variable.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "InstrumentSerif";
|
||||
src: url("/assets/fonts/InstrumentSerif-Regular.ttf");
|
||||
font-display: swap;
|
||||
font-family: 'InstrumentSerif';
|
||||
src: url('/assets/fonts/InstrumentSerif-Regular.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.font-secondary {
|
||||
font-family: "InstrumentSerif", sans-serif;
|
||||
font-family: 'InstrumentSerif', sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Archivo";
|
||||
font-family: 'Archivo';
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-*: initial;
|
||||
--color-*: initial;
|
||||
|
||||
--color-white: #fff;
|
||||
--color-gray-50: #f9f9f9;
|
||||
--color-gray-100: #ececec;
|
||||
--color-gray-200: #e3e3e3;
|
||||
--color-gray-300: #cdcdcd;
|
||||
--color-gray-400: #b4b4b4;
|
||||
--color-gray-500: #9b9b9b;
|
||||
--color-gray-600: #676767;
|
||||
--color-gray-700: #4e4e4e;
|
||||
--color-gray-800: #333;
|
||||
--color-gray-850: #262626;
|
||||
--color-gray-900: #171717;
|
||||
--color-gray-950: #0d0d0d;
|
||||
--color-white: #fff;
|
||||
--color-black: #000;
|
||||
--color-gray-50: #f9f9f9;
|
||||
--color-gray-100: #ececec;
|
||||
--color-gray-200: #e3e3e3;
|
||||
--color-gray-300: #cdcdcd;
|
||||
--color-gray-400: #b4b4b4;
|
||||
--color-gray-500: #9b9b9b;
|
||||
--color-gray-600: #676767;
|
||||
--color-gray-700: #4e4e4e;
|
||||
--color-gray-800: #333;
|
||||
--color-gray-850: #262626;
|
||||
--color-gray-900: #171717;
|
||||
--color-gray-950: #0d0d0d;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='dark'] {
|
||||
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
|
||||
}
|
||||
|
||||
64
src/render/lib/components/ListView.svelte
Normal file
64
src/render/lib/components/ListView.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from './common/Tooltip.svelte';
|
||||
import Plus from './icons/Plus.svelte';
|
||||
|
||||
let selected = 'home';
|
||||
</script>
|
||||
|
||||
<div class="min-w-18 bg-gray-950 flex gap-2.5 flex-col pt-9.5">
|
||||
<div class="flex justify-center relative">
|
||||
{#if selected === 'home'}
|
||||
<div class="absolute top-0 left-0 flex h-full">
|
||||
<div class="my-auto rounded-r-lg w-1 h-8 bg-white"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Tooltip content="Home" placement="right">
|
||||
<button
|
||||
class=" cursor-pointer bg-gray-850 {selected === 'home'
|
||||
? 'rounded-2xl'
|
||||
: 'rounded-full'}"
|
||||
onclick={() => {
|
||||
selected = 'home';
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./assets/images/splash.png"
|
||||
class="size-11 dark:invert p-1"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-900 mx-3"></div>
|
||||
|
||||
<!-- <div class="flex justify-center relative group">
|
||||
{#if selected === ""}
|
||||
<div class="absolute top-0 left-0 flex h-full">
|
||||
<div class="my-auto rounded-r-lg w-1 h-8 bg-white"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class=" cursor-pointer bg-transparent"
|
||||
onclick={() => {
|
||||
selected = "";
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./assets/images/adam.jpg"
|
||||
class="size-11 {selected === '' ? 'rounded-2xl' : 'rounded-full'}"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
</div> -->
|
||||
|
||||
<div class="flex justify-center relative group text-gray-400">
|
||||
<button class=" cursor-pointer p-2" onclick={() => {}}>
|
||||
<Plus className="size-5" strokeWidth="2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,76 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import Spinner from "./common/Spinner.svelte";
|
||||
import Spinner from './common/Spinner.svelte';
|
||||
import ListView from './ListView.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-full h-full relative dark:text-gray-100">
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-6 bg-gray-900 bg-opacity-50 draggable"
|
||||
></div>
|
||||
<div class="m-auto flex flex-col max-w-xs w-full text-center">
|
||||
<div class=" flex justify-center mb-3">
|
||||
<img
|
||||
src="./assets/images/splash.png"
|
||||
class=" size-24 dark:invert"
|
||||
alt="hero"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute top-0 left-0 w-full h-6 bg-transparent draggable"></div>
|
||||
|
||||
<!-- <div class=" text-2xl text-gray-50 font-secondary">Install Open WebUI</div> -->
|
||||
<ListView />
|
||||
|
||||
<div class=" text-gray-500 hover:text-white transition">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<div>Loading...</div>
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 w-full flex justify-center">
|
||||
<div class="my-auto flex flex-col max-w-xs w-full">
|
||||
<div class=" flex justify-center mb-3">
|
||||
<!-- <img
|
||||
src="./assets/images/splash.png"
|
||||
class=" size-24 dark:invert"
|
||||
alt="hero"
|
||||
/> -->
|
||||
</div>
|
||||
|
||||
<!-- <button
|
||||
class=" hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("install clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.installPackage();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<div>Install</div>
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</button> -->
|
||||
<!-- <div class=" text-2xl text-gray-50 font-secondary">Install Open WebUI</div> -->
|
||||
|
||||
<!--
|
||||
<div class=" text-gray-500 hover:text-white transition">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<div>Loading...</div>
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("start clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.startServer();
|
||||
}
|
||||
}}>Start Open WebUI</button
|
||||
>
|
||||
<!-- <button
|
||||
class=" hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("install clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.installPackage();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<div>Install</div>
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</button> -->
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("stop clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.stopServer();
|
||||
}
|
||||
}}>Stop Open WebUI</button
|
||||
> -->
|
||||
</div>
|
||||
<!--
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("start clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.startServer();
|
||||
}
|
||||
}}>Start Open WebUI</button
|
||||
>
|
||||
|
||||
<button
|
||||
class=" text-gray-100 hover:text-white transition font-medium cursor-pointer"
|
||||
onclick={() => {
|
||||
console.log("stop clicked");
|
||||
if (window?.electronAPI) {
|
||||
window.electronAPI.stopServer();
|
||||
}
|
||||
}}>Stop Open WebUI</button
|
||||
> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.draggable {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.draggable {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
export let className: string = "size-5";
|
||||
export let className: string = 'size-5';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center text-center">
|
||||
<svg
|
||||
class={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<svg
|
||||
class={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
|
||||
52
src/render/lib/components/common/Tooltip.svelte
Normal file
52
src/render/lib/components/common/Tooltip.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { onDestroy } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import tippy from 'tippy.js';
|
||||
import { roundArrow } from 'tippy.js';
|
||||
|
||||
export let placement = 'top';
|
||||
export let content = `I'm a tooltip!`;
|
||||
export let touch = true;
|
||||
export let className = 'flex';
|
||||
export let theme = '';
|
||||
export let offset = [0, 4];
|
||||
export let allowHTML = true;
|
||||
export let tippyOptions = {};
|
||||
|
||||
let tooltipElement;
|
||||
let tooltipInstance;
|
||||
|
||||
$: if (tooltipElement && content) {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.setContent(DOMPurify.sanitize(content));
|
||||
} else {
|
||||
tooltipInstance = tippy(tooltipElement, {
|
||||
content: DOMPurify.sanitize(content),
|
||||
placement: placement,
|
||||
allowHTML: allowHTML,
|
||||
touch: touch,
|
||||
...(theme !== '' ? { theme } : { theme: 'dark' }),
|
||||
arrow: false,
|
||||
offset: offset,
|
||||
...tippyOptions
|
||||
});
|
||||
}
|
||||
} else if (tooltipInstance && content === '') {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={tooltipElement} aria-label={DOMPurify.sanitize(content)} class={className}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,19 +1,15 @@
|
||||
<script lang="ts">
|
||||
export let className = "w-4 h-4";
|
||||
export let strokeWidth = "2";
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '2';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4.5v15m7.5-7.5h-15"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { mount } from 'svelte'
|
||||
import './render/app.css'
|
||||
import App from './render/App.svelte'
|
||||
import { mount } from 'svelte';
|
||||
import './render/app.css';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
|
||||
import App from './render/App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
target: document.getElementById('app')
|
||||
});
|
||||
|
||||
export default app
|
||||
export default app;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
exec,
|
||||
execFile,
|
||||
ExecFileOptions,
|
||||
execFileSync,
|
||||
execSync,
|
||||
spawn,
|
||||
ChildProcess,
|
||||
} from "child_process";
|
||||
import net from "net";
|
||||
exec,
|
||||
execFile,
|
||||
ExecFileOptions,
|
||||
execFileSync,
|
||||
execSync,
|
||||
spawn,
|
||||
ChildProcess
|
||||
} from 'child_process';
|
||||
import net from 'net';
|
||||
|
||||
import * as tar from "tar";
|
||||
import log from "electron-log";
|
||||
import * as tar from 'tar';
|
||||
import log from 'electron-log';
|
||||
|
||||
import { app } from "electron";
|
||||
import { app } from 'electron';
|
||||
|
||||
////////////////////////////////////////////////
|
||||
//
|
||||
@@ -24,73 +24,70 @@ import { app } from "electron";
|
||||
////////////////////////////////////////////////
|
||||
|
||||
export function getAppPath(): string {
|
||||
let appDir = app.getAppPath();
|
||||
return appDir;
|
||||
let appDir = app.getAppPath();
|
||||
return appDir;
|
||||
}
|
||||
|
||||
export function getUserHomePath(): string {
|
||||
return app.getPath("home");
|
||||
return app.getPath('home');
|
||||
}
|
||||
|
||||
export function getUserDataPath(): string {
|
||||
const userDataDir = app.getPath("userData");
|
||||
const userDataDir = app.getPath('userData');
|
||||
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
try {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
try {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return userDataDir;
|
||||
return userDataDir;
|
||||
}
|
||||
|
||||
export function getOpenWebUIDataPath(): string {
|
||||
const openWebUIDataDir = path.join(getUserDataPath(), "data");
|
||||
const openWebUIDataDir = path.join(getUserDataPath(), 'data');
|
||||
|
||||
if (!fs.existsSync(openWebUIDataDir)) {
|
||||
try {
|
||||
fs.mkdirSync(openWebUIDataDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(openWebUIDataDir)) {
|
||||
try {
|
||||
fs.mkdirSync(openWebUIDataDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return openWebUIDataDir;
|
||||
return openWebUIDataDir;
|
||||
}
|
||||
|
||||
export async function portInUse(
|
||||
port: number,
|
||||
host: string = "127.0.0.1"
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const client = new net.Socket();
|
||||
export async function portInUse(port: number, host: string = '127.0.0.1'): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const client = new net.Socket();
|
||||
|
||||
// Attempt to connect to the port
|
||||
client
|
||||
.setTimeout(1000) // Timeout for the connection attempt
|
||||
.once("connect", () => {
|
||||
// If connection succeeds, port is in use
|
||||
client.destroy();
|
||||
resolve(true);
|
||||
})
|
||||
.once("timeout", () => {
|
||||
// If no connection after the timeout, port is not in use
|
||||
client.destroy();
|
||||
resolve(false);
|
||||
})
|
||||
.once("error", (err: any) => {
|
||||
if (err.code === "ECONNREFUSED") {
|
||||
// Port is not in use or no listener is accepting connections
|
||||
resolve(false);
|
||||
} else {
|
||||
// Unexpected error
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.connect(port, host);
|
||||
});
|
||||
// Attempt to connect to the port
|
||||
client
|
||||
.setTimeout(1000) // Timeout for the connection attempt
|
||||
.once('connect', () => {
|
||||
// If connection succeeds, port is in use
|
||||
client.destroy();
|
||||
resolve(true);
|
||||
})
|
||||
.once('timeout', () => {
|
||||
// If no connection after the timeout, port is not in use
|
||||
client.destroy();
|
||||
resolve(false);
|
||||
})
|
||||
.once('error', (err: any) => {
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
// Port is not in use or no listener is accepting connections
|
||||
resolve(false);
|
||||
} else {
|
||||
// Unexpected error
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.connect(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@@ -100,43 +97,43 @@ export async function portInUse(
|
||||
////////////////////////////////////////////////
|
||||
|
||||
export function getBundledPythonTarPath(): string {
|
||||
const appPath = getAppPath();
|
||||
return path.join(appPath, "resources", "python.tar.gz");
|
||||
const appPath = getAppPath();
|
||||
return path.join(appPath, 'resources', 'python.tar.gz');
|
||||
}
|
||||
|
||||
export function getBundledPythonInstallationPath(): string {
|
||||
const installDir = path.join(app.getPath("userData"), "python");
|
||||
const installDir = path.join(app.getPath('userData'), 'python');
|
||||
|
||||
if (!fs.existsSync(installDir)) {
|
||||
try {
|
||||
fs.mkdirSync(installDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
return installDir;
|
||||
if (!fs.existsSync(installDir)) {
|
||||
try {
|
||||
fs.mkdirSync(installDir, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
return installDir;
|
||||
}
|
||||
|
||||
export function isCondaEnv(envPath: string): boolean {
|
||||
return fs.existsSync(path.join(envPath, "conda-meta"));
|
||||
return fs.existsSync(path.join(envPath, 'conda-meta'));
|
||||
}
|
||||
|
||||
export function getPythonPath(envPath: string, isConda?: boolean) {
|
||||
if (process.platform === "win32") {
|
||||
return isConda ?? isCondaEnv(envPath)
|
||||
? path.join(envPath, "python.exe")
|
||||
: path.join(envPath, "Scripts", "python.exe");
|
||||
} else {
|
||||
return path.join(envPath, "bin", "python");
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return (isConda ?? isCondaEnv(envPath))
|
||||
? path.join(envPath, 'python.exe')
|
||||
: path.join(envPath, 'Scripts', 'python.exe');
|
||||
} else {
|
||||
return path.join(envPath, 'bin', 'python');
|
||||
}
|
||||
}
|
||||
|
||||
export function getBundledPythonPath() {
|
||||
return getPythonPath(getBundledPythonInstallationPath());
|
||||
return getPythonPath(getBundledPythonInstallationPath());
|
||||
}
|
||||
|
||||
export function isBundledPythonInstalled() {
|
||||
return fs.existsSync(getBundledPythonPath());
|
||||
return fs.existsSync(getBundledPythonPath());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@@ -153,133 +150,131 @@ export function isBundledPythonInstalled() {
|
||||
////////////////////////////////////////////////
|
||||
|
||||
export function createAdHocSignCommand(envPath: string): string {
|
||||
const appPath = getAppPath();
|
||||
const appPath = getAppPath();
|
||||
|
||||
const signListFile = path.join(
|
||||
appPath,
|
||||
"resources",
|
||||
`sign-osx-${process.arch === "arm64" ? "arm64" : "64"}.txt`
|
||||
);
|
||||
const fileContents = fs.readFileSync(signListFile, "utf-8");
|
||||
const signList: string[] = [];
|
||||
const signListFile = path.join(
|
||||
appPath,
|
||||
'resources',
|
||||
`sign-osx-${process.arch === 'arm64' ? 'arm64' : '64'}.txt`
|
||||
);
|
||||
const fileContents = fs.readFileSync(signListFile, 'utf-8');
|
||||
const signList: string[] = [];
|
||||
|
||||
fileContents.split(/\r?\n/).forEach((line) => {
|
||||
if (line) {
|
||||
signList.push(`"${line}"`);
|
||||
}
|
||||
});
|
||||
fileContents.split(/\r?\n/).forEach((line) => {
|
||||
if (line) {
|
||||
signList.push(`"${line}"`);
|
||||
}
|
||||
});
|
||||
|
||||
// sign all binaries with ad-hoc signature
|
||||
return `cd ${envPath} && codesign -s - -o 0x2 -f ${signList.join(
|
||||
" "
|
||||
)} && cd -`;
|
||||
// sign all binaries with ad-hoc signature
|
||||
return `cd ${envPath} && codesign -s - -o 0x2 -f ${signList.join(' ')} && cd -`;
|
||||
}
|
||||
|
||||
export async function installOpenWebUI(installationPath: string) {
|
||||
console.log(installationPath);
|
||||
let unpackCommand =
|
||||
process.platform === "win32"
|
||||
? `${installationPath}\\Scripts\\activate.bat && uv pip install open-webui -U`
|
||||
: `source "${installationPath}/bin/activate" && uv pip install open-webui -U`;
|
||||
console.log(installationPath);
|
||||
let unpackCommand =
|
||||
process.platform === 'win32'
|
||||
? `${installationPath}\\Scripts\\activate.bat && uv pip install open-webui -U`
|
||||
: `source "${installationPath}/bin/activate" && uv pip install open-webui -U`;
|
||||
|
||||
// only unsign when installing from bundled installer
|
||||
// if (platform === "darwin") {
|
||||
// unpackCommand = `${createAdHocSignCommand(installationPath)}\n${unpackCommand}`;
|
||||
// }
|
||||
// only unsign when installing from bundled installer
|
||||
// if (platform === "darwin") {
|
||||
// unpackCommand = `${createAdHocSignCommand(installationPath)}\n${unpackCommand}`;
|
||||
// }
|
||||
|
||||
console.log(unpackCommand);
|
||||
console.log(unpackCommand);
|
||||
|
||||
const commandProcess = exec(unpackCommand, {
|
||||
shell: process.platform === "win32" ? "cmd.exe" : "/bin/bash",
|
||||
});
|
||||
const commandProcess = exec(unpackCommand, {
|
||||
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'
|
||||
});
|
||||
|
||||
commandProcess.stdout?.on("data", (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
commandProcess.stdout?.on('data', (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
commandProcess.stderr?.on("data", (data) => {
|
||||
console.error(data);
|
||||
});
|
||||
commandProcess.stderr?.on('data', (data) => {
|
||||
console.error(data);
|
||||
});
|
||||
|
||||
commandProcess.on("exit", (code) => {
|
||||
console.log(`Child exited with code ${code}`);
|
||||
});
|
||||
commandProcess.on('exit', (code) => {
|
||||
console.log(`Child exited with code ${code}`);
|
||||
});
|
||||
}
|
||||
|
||||
export async function installBundledPython(installationPath?: string) {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
|
||||
const pythonTarPath = getBundledPythonTarPath();
|
||||
const pythonTarPath = getBundledPythonTarPath();
|
||||
|
||||
console.log(installationPath, pythonTarPath);
|
||||
if (!fs.existsSync(pythonTarPath)) {
|
||||
log.error("Python tarball not found");
|
||||
return;
|
||||
}
|
||||
console.log(installationPath, pythonTarPath);
|
||||
if (!fs.existsSync(pythonTarPath)) {
|
||||
log.error('Python tarball not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(installationPath, { recursive: true });
|
||||
await tar.x({
|
||||
cwd: installationPath,
|
||||
file: pythonTarPath,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
try {
|
||||
fs.mkdirSync(installationPath, { recursive: true });
|
||||
await tar.x({
|
||||
cwd: installationPath,
|
||||
file: pythonTarPath
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
// Get the path to the installed Python binary
|
||||
const bundledPythonPath = getBundledPythonPath();
|
||||
// Get the path to the installed Python binary
|
||||
const bundledPythonPath = getBundledPythonPath();
|
||||
|
||||
if (!fs.existsSync(bundledPythonPath)) {
|
||||
log.error("Python binary not found in install path");
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(bundledPythonPath)) {
|
||||
log.error('Python binary not found in install path');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the Python binary to print the version
|
||||
const pythonVersion = execFileSync(bundledPythonPath, ["--version"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
console.log("Installed Python Version:", pythonVersion.trim());
|
||||
} catch (error) {
|
||||
log.error("Failed to execute Python binary", error);
|
||||
}
|
||||
try {
|
||||
// Execute the Python binary to print the version
|
||||
const pythonVersion = execFileSync(bundledPythonPath, ['--version'], {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
console.log('Installed Python Version:', pythonVersion.trim());
|
||||
} catch (error) {
|
||||
log.error('Failed to execute Python binary', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installPackage(installationPath?: string) {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
|
||||
// if (!isBundledPythonInstalled()) {
|
||||
// try {
|
||||
// await installBundledPython(installationPath);
|
||||
// } catch (error) {
|
||||
// log.error("Failed to install bundled Python", error);
|
||||
// return Promise.reject("Failed to install bundled Python");
|
||||
// }
|
||||
// }
|
||||
// if (!isBundledPythonInstalled()) {
|
||||
// try {
|
||||
// await installBundledPython(installationPath);
|
||||
// } catch (error) {
|
||||
// log.error("Failed to install bundled Python", error);
|
||||
// return Promise.reject("Failed to install bundled Python");
|
||||
// }
|
||||
// }
|
||||
|
||||
try {
|
||||
await installBundledPython(installationPath);
|
||||
} catch (error) {
|
||||
log.error("Failed to install bundled Python", error);
|
||||
return Promise.reject("Failed to install bundled Python");
|
||||
}
|
||||
try {
|
||||
await installBundledPython(installationPath);
|
||||
} catch (error) {
|
||||
log.error('Failed to install bundled Python', error);
|
||||
return Promise.reject('Failed to install bundled Python');
|
||||
}
|
||||
|
||||
try {
|
||||
await installOpenWebUI(installationPath);
|
||||
} catch (error) {
|
||||
log.error("Failed to install open-webui", error);
|
||||
return Promise.reject("Failed to install open-webui");
|
||||
}
|
||||
try {
|
||||
await installOpenWebUI(installationPath);
|
||||
} catch (error) {
|
||||
log.error('Failed to install open-webui', error);
|
||||
return Promise.reject('Failed to install open-webui');
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePackage(installationPath?: string) {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
|
||||
// remove the python env entirely
|
||||
if (fs.existsSync(installationPath)) {
|
||||
fs.rmdirSync(installationPath, { recursive: true });
|
||||
}
|
||||
// remove the python env entirely
|
||||
if (fs.existsSync(installationPath)) {
|
||||
fs.rmdirSync(installationPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@@ -292,25 +287,23 @@ export async function removePackage(installationPath?: string) {
|
||||
* Validates that Python is installed and the `open-webui` package is present
|
||||
* within the specified virtual environment.
|
||||
*/
|
||||
export async function validateInstallation(
|
||||
installationPath?: string
|
||||
): Promise<boolean> {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
const pythonPath = getPythonPath(installationPath);
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const checkCommand =
|
||||
process.platform === "win32"
|
||||
? `${installationPath}\\Scripts\\activate.bat && pip show open-webui`
|
||||
: `source "${installationPath}/bin/activate" && pip show open-webui`;
|
||||
execSync(checkCommand, { stdio: "ignore" });
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
export async function validateInstallation(installationPath?: string): Promise<boolean> {
|
||||
installationPath = installationPath || getBundledPythonInstallationPath();
|
||||
const pythonPath = getPythonPath(installationPath);
|
||||
if (!fs.existsSync(pythonPath)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const checkCommand =
|
||||
process.platform === 'win32'
|
||||
? `${installationPath}\\Scripts\\activate.bat && pip show open-webui`
|
||||
: `source "${installationPath}/bin/activate" && pip show open-webui`;
|
||||
execSync(checkCommand, { stdio: 'ignore' });
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tracks all spawned server process PIDs
|
||||
@@ -319,144 +312,137 @@ const serverPIDs: Set<number> = new Set();
|
||||
/**
|
||||
* Spawn the Open-WebUI server process.
|
||||
*/
|
||||
export async function startServer(
|
||||
installationPath?: string,
|
||||
port?: number
|
||||
): Promise<string> {
|
||||
installationPath = path.normalize(
|
||||
installationPath || getBundledPythonInstallationPath()
|
||||
);
|
||||
export async function startServer(installationPath?: string, port?: number): Promise<string> {
|
||||
installationPath = path.normalize(installationPath || getBundledPythonInstallationPath());
|
||||
|
||||
if (!validateInstallation(installationPath)) {
|
||||
console.error("Failed to validate installation");
|
||||
return;
|
||||
}
|
||||
if (!validateInstallation(installationPath)) {
|
||||
console.error('Failed to validate installation');
|
||||
return;
|
||||
}
|
||||
|
||||
let startCommand =
|
||||
process.platform === "win32"
|
||||
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
|
||||
app.getPath("userData"),
|
||||
"data"
|
||||
)}" && open-webui serve`
|
||||
: `source "${installationPath}/bin/activate" && export DATA_DIR="${path.join(
|
||||
app.getPath("userData"),
|
||||
"data"
|
||||
)}" && open-webui serve`;
|
||||
let startCommand =
|
||||
process.platform === 'win32'
|
||||
? `${installationPath}\\Scripts\\activate.bat && set DATA_DIR="${path.join(
|
||||
app.getPath('userData'),
|
||||
'data'
|
||||
)}" && open-webui serve`
|
||||
: `source "${installationPath}/bin/activate" && export DATA_DIR="${path.join(
|
||||
app.getPath('userData'),
|
||||
'data'
|
||||
)}" && open-webui serve`;
|
||||
|
||||
port = port || 8080;
|
||||
while (await portInUse(port)) {
|
||||
port++;
|
||||
}
|
||||
port = port || 8080;
|
||||
while (await portInUse(port)) {
|
||||
port++;
|
||||
}
|
||||
|
||||
startCommand += ` --port ${port}`;
|
||||
startCommand += ` --port ${port}`;
|
||||
|
||||
console.log("Starting Open-WebUI server...");
|
||||
const childProcess = spawn(startCommand, {
|
||||
shell: true,
|
||||
detached: true,
|
||||
stdio: ["ignore", "pipe", "pipe"], // Let us capture logs via stdout/stderr
|
||||
});
|
||||
console.log('Starting Open-WebUI server...');
|
||||
const childProcess = spawn(startCommand, {
|
||||
shell: true,
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'] // Let us capture logs via stdout/stderr
|
||||
});
|
||||
|
||||
let serverCrashed = false;
|
||||
let detectedURL: string | null = null;
|
||||
let serverCrashed = false;
|
||||
let detectedURL: string | null = null;
|
||||
|
||||
// Wait for log output to confirm the server has started
|
||||
async function monitorServerLogs(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const handleLog = (data: Buffer) => {
|
||||
const logLine = data.toString().trim();
|
||||
console.log(`[Open-WebUI Log]: ${logLine}`);
|
||||
// Wait for log output to confirm the server has started
|
||||
async function monitorServerLogs(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const handleLog = (data: Buffer) => {
|
||||
const logLine = data.toString().trim();
|
||||
console.log(`[Open-WebUI Log]: ${logLine}`);
|
||||
|
||||
// Look for "Uvicorn running on http://<hostname>:<port>"
|
||||
const match = logLine.match(
|
||||
/Uvicorn running on (http:\/\/[^\s]+) \(Press CTRL\+C to quit\)/
|
||||
);
|
||||
if (match) {
|
||||
detectedURL = match[1]; // e.g., "http://0.0.0.0:8081"
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
// Look for "Uvicorn running on http://<hostname>:<port>"
|
||||
const match = logLine.match(
|
||||
/Uvicorn running on (http:\/\/[^\s]+) \(Press CTRL\+C to quit\)/
|
||||
);
|
||||
if (match) {
|
||||
detectedURL = match[1]; // e.g., "http://0.0.0.0:8081"
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Combine stdout and stderr streams as a unified log source
|
||||
childProcess.stdout?.on("data", handleLog);
|
||||
childProcess.stderr?.on("data", handleLog);
|
||||
// Combine stdout and stderr streams as a unified log source
|
||||
childProcess.stdout?.on('data', handleLog);
|
||||
childProcess.stderr?.on('data', handleLog);
|
||||
|
||||
childProcess.on("close", (code) => {
|
||||
serverCrashed = true;
|
||||
if (!detectedURL) {
|
||||
reject(
|
||||
new Error(
|
||||
`Process exited unexpectedly with code ${code}. No server URL detected.`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
childProcess.on('close', (code) => {
|
||||
serverCrashed = true;
|
||||
if (!detectedURL) {
|
||||
reject(
|
||||
new Error(
|
||||
`Process exited unexpectedly with code ${code}. No server URL detected.`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Track the child process PID
|
||||
if (childProcess.pid) {
|
||||
serverPIDs.add(childProcess.pid);
|
||||
console.log(`Server started with PID: ${childProcess.pid}`);
|
||||
} else {
|
||||
throw new Error("Failed to start server: No PID available");
|
||||
}
|
||||
// Track the child process PID
|
||||
if (childProcess.pid) {
|
||||
serverPIDs.add(childProcess.pid);
|
||||
console.log(`Server started with PID: ${childProcess.pid}`);
|
||||
} else {
|
||||
throw new Error('Failed to start server: No PID available');
|
||||
}
|
||||
|
||||
// Wait until the server log confirms it's started
|
||||
try {
|
||||
await monitorServerLogs();
|
||||
} catch (error) {
|
||||
if (serverCrashed) {
|
||||
throw new Error("Server crashed unexpectedly.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
// Wait until the server log confirms it's started
|
||||
try {
|
||||
await monitorServerLogs();
|
||||
} catch (error) {
|
||||
if (serverCrashed) {
|
||||
throw new Error('Server crashed unexpectedly.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!detectedURL) {
|
||||
throw new Error("Failed to detect server URL from logs.");
|
||||
}
|
||||
if (!detectedURL) {
|
||||
throw new Error('Failed to detect server URL from logs.');
|
||||
}
|
||||
|
||||
console.log(`Server is now running at ${detectedURL}`);
|
||||
return detectedURL; // Return the detected URL
|
||||
console.log(`Server is now running at ${detectedURL}`);
|
||||
return detectedURL; // Return the detected URL
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates all server processes.
|
||||
*/
|
||||
export async function stopAllServers(): Promise<void> {
|
||||
console.log("Stopping all servers...");
|
||||
for (const pid of serverPIDs) {
|
||||
try {
|
||||
terminateProcessTree(pid);
|
||||
serverPIDs.delete(pid); // Remove from tracking set after termination
|
||||
} catch (error) {
|
||||
console.error(`Error stopping server with PID ${pid}:`, error);
|
||||
}
|
||||
}
|
||||
console.log("All servers stopped successfully.");
|
||||
console.log('Stopping all servers...');
|
||||
for (const pid of serverPIDs) {
|
||||
try {
|
||||
terminateProcessTree(pid);
|
||||
serverPIDs.delete(pid); // Remove from tracking set after termination
|
||||
} catch (error) {
|
||||
console.error(`Error stopping server with PID ${pid}:`, error);
|
||||
}
|
||||
}
|
||||
console.log('All servers stopped successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a process tree by PID.
|
||||
*/
|
||||
function terminateProcessTree(pid: number): void {
|
||||
if (process.platform === "win32") {
|
||||
// Use `taskkill` on Windows to recursively kill the process and its children
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`); // /T -> terminate child processes, /F -> force termination
|
||||
console.log(`Terminated server process tree (PID: ${pid}) on Windows.`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
|
||||
}
|
||||
} else {
|
||||
// Use `kill` on Unix-like platforms to terminate the process group (-pid)
|
||||
try {
|
||||
process.kill(-pid, "SIGKILL"); // Negative PID (-pid) kills the process group
|
||||
console.log(
|
||||
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
|
||||
}
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
// Use `taskkill` on Windows to recursively kill the process and its children
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`); // /T -> terminate child processes, /F -> force termination
|
||||
console.log(`Terminated server process tree (PID: ${pid}) on Windows.`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
|
||||
}
|
||||
} else {
|
||||
// Use `kill` on Unix-like platforms to terminate the process group (-pid)
|
||||
try {
|
||||
process.kill(-pid, 'SIGKILL'); // Negative PID (-pid) kills the process group
|
||||
console.log(`Terminated server process tree (PID: ${pid}) on Unix-like OS.`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to terminate process tree (PID: ${pid}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), svelte()],
|
||||
plugins: [tailwindcss(), svelte()],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user