chore: format

This commit is contained in:
Timothy Jaeryang Baek 2025-01-11 18:13:43 -08:00
parent 1825957bd3
commit e91a8ab4a7
24 changed files with 14064 additions and 13851 deletions

View File

@ -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
View 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
View File

@ -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

View File

@ -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
View File

@ -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! 🚀

View File

@ -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;

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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);
}

View File

@ -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');
}
});

View File

@ -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>

View File

@ -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;
}

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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()
};

View File

@ -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
}
}

View File

@ -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()],
});